diff --git a/apps/docs/spec/common-api-sections.json b/apps/docs/spec/common-api-sections.json
index 733b6ffb8149b..0e81bfb4e9400 100644
--- a/apps/docs/spec/common-api-sections.json
+++ b/apps/docs/spec/common-api-sections.json
@@ -196,12 +196,6 @@
"slug": "v1-apply-a-migration",
"type": "operation"
},
- {
- "id": "v1-create-restore-point",
- "title": "Create restore point",
- "slug": "v1-create-restore-point",
- "type": "operation"
- },
{
"id": "v1-disable-readonly-mode-temporarily",
"title": "Disable readonly mode temporarily",
@@ -256,12 +250,6 @@
"slug": "v1-get-readonly-mode-status",
"type": "operation"
},
- {
- "id": "v1-get-restore-point",
- "title": "Get restore point",
- "slug": "v1-get-restore-point",
- "type": "operation"
- },
{
"id": "v1-get-ssl-enforcement-config",
"title": "Get ssl enforcement config",
@@ -310,12 +298,6 @@
"slug": "v1-setup-a-read-replica",
"type": "operation"
},
- {
- "id": "v1-undo",
- "title": "Undo",
- "slug": "v1-undo",
- "type": "operation"
- },
{
"id": "v1-update-pooler-config",
"title": "Update pooler config",
@@ -538,12 +520,6 @@
"slug": "v1-exchange-oauth-token",
"type": "operation"
},
- {
- "id": "v1-oauth-authorize-project-claim",
- "title": "Oauth authorize project claim",
- "slug": "v1-oauth-authorize-project-claim",
- "type": "operation"
- },
{
"id": "v1-revoke-token",
"title": "Revoke token",
@@ -556,12 +532,6 @@
"type": "category",
"title": "Organizations",
"items": [
- {
- "id": "v1-claim-project-for-organization",
- "title": "Claim project for organization",
- "slug": "v1-claim-project-for-organization",
- "type": "operation"
- },
{
"id": "v1-create-an-organization",
"title": "Create an organization",
@@ -574,12 +544,6 @@
"slug": "v1-get-an-organization",
"type": "operation"
},
- {
- "id": "v1-get-organization-project-claim",
- "title": "Get organization project claim",
- "slug": "v1-get-organization-project-claim",
- "type": "operation"
- },
{
"id": "v1-list-all-organizations",
"title": "List all organizations",
@@ -610,12 +574,6 @@
"slug": "v1-create-a-project",
"type": "operation"
},
- {
- "id": "v1-create-project-claim-token",
- "title": "Create project claim token",
- "slug": "v1-create-project-claim-token",
- "type": "operation"
- },
{
"id": "v1-delete-a-project",
"title": "Delete a project",
@@ -628,12 +586,6 @@
"slug": "v1-delete-network-bans",
"type": "operation"
},
- {
- "id": "v1-delete-project-claim-token",
- "title": "Delete project claim token",
- "slug": "v1-delete-project-claim-token",
- "type": "operation"
- },
{
"id": "v1-get-network-restrictions",
"title": "Get network restrictions",
@@ -658,12 +610,6 @@
"slug": "v1-get-project",
"type": "operation"
},
- {
- "id": "v1-get-project-claim-token",
- "title": "Get project claim token",
- "slug": "v1-get-project-claim-token",
- "type": "operation"
- },
{
"id": "v1-get-services-health",
"title": "Get services health",
diff --git a/apps/docs/spec/sections/generateMgmtApiSections.cts b/apps/docs/spec/sections/generateMgmtApiSections.cts
index 580acb24866ee..c235b7eccf2ef 100644
--- a/apps/docs/spec/sections/generateMgmtApiSections.cts
+++ b/apps/docs/spec/sections/generateMgmtApiSections.cts
@@ -41,6 +41,12 @@ function extractSectionsFromOpenApi(filePath, outputPath) {
for (const route in openApiJson.paths) {
const methods = openApiJson.paths[route]
for (const method in methods) {
+ // We are using `x-internal` to hide endpoints from the docs,
+ // but still have them included in the spec so they generate types and can be used.
+ if (methods[method]['x-internal']) {
+ continue
+ }
+
const tag = methods[method].tags?.[0]
const operationId = methods[method].operationId
// If operationId is not in the form of a slug ignore it.
diff --git a/apps/docs/spec/supabase_dart_v2.yml b/apps/docs/spec/supabase_dart_v2.yml
index 2a1dd1b2d4e1b..6219bea93b790 100644
--- a/apps/docs/spec/supabase_dart_v2.yml
+++ b/apps/docs/spec/supabase_dart_v2.yml
@@ -1341,7 +1341,7 @@ functions:
isSpotlight: true
code: |
```dart
- // retrieve all identites linked to a user
+ // retrieve all identities linked to a user
final identities = await supabase.auth.getUserIdentities();
// find the google identity
diff --git a/apps/docs/spec/supabase_js_v2.yml b/apps/docs/spec/supabase_js_v2.yml
index dcaf090330929..2e6c387f8d8a9 100644
--- a/apps/docs/spec/supabase_js_v2.yml
+++ b/apps/docs/spec/supabase_js_v2.yml
@@ -1625,7 +1625,7 @@ functions:
isSpotlight: true
code: |
```js
- // retrieve all identites linked to a user
+ // retrieve all identities linked to a user
const identities = await supabase.auth.getUserIdentities()
// find the google identity
diff --git a/apps/docs/spec/supabase_py_v2.yml b/apps/docs/spec/supabase_py_v2.yml
index 661cf1e884e8d..31f21f040112f 100644
--- a/apps/docs/spec/supabase_py_v2.yml
+++ b/apps/docs/spec/supabase_py_v2.yml
@@ -1550,7 +1550,7 @@ functions:
isSpotlight: true
code: |
```python
- # retrieve all identites linked to a user
+ # retrieve all identities linked to a user
response = supabase.auth.get_user_identities()
# find the google identity
diff --git a/apps/docs/spec/supabase_swift_v2.yml b/apps/docs/spec/supabase_swift_v2.yml
index 37e8587aabd57..9970a629f1209 100644
--- a/apps/docs/spec/supabase_swift_v2.yml
+++ b/apps/docs/spec/supabase_swift_v2.yml
@@ -944,7 +944,7 @@ functions:
isSpotlight: true
code: |
```swift
- // retrieve all identites linked to a user
+ // retrieve all identities linked to a user
let identities = try await supabase.auth.userIdentities()
// find the google identity
diff --git a/apps/studio/components/interfaces/Home/Home.tsx b/apps/studio/components/interfaces/Home/Home.tsx
new file mode 100644
index 0000000000000..014d8749a84a5
--- /dev/null
+++ b/apps/studio/components/interfaces/Home/Home.tsx
@@ -0,0 +1,297 @@
+import dayjs from 'dayjs'
+import Link from 'next/link'
+import { useEffect, useMemo, useRef } from 'react'
+
+import { useParams } from 'common'
+import { ClientLibrary } from 'components/interfaces/Home'
+import { AdvisorWidget } from 'components/interfaces/Home/AdvisorWidget'
+import { ExampleProject } from 'components/interfaces/Home/ExampleProject'
+import { CLIENT_LIBRARIES, EXAMPLE_PROJECTS } from 'components/interfaces/Home/Home.constants'
+import { NewProjectPanel } from 'components/interfaces/Home/NewProjectPanel/NewProjectPanel'
+import { ProjectUsageSection } from 'components/interfaces/Home/ProjectUsageSection'
+import { ServiceStatus } from 'components/interfaces/Home/ServiceStatus'
+import { ProjectPausedState } from 'components/layouts/ProjectLayout/PausedState/ProjectPausedState'
+import { ComputeBadgeWrapper } from 'components/ui/ComputeBadgeWrapper'
+import { InlineLink } from 'components/ui/InlineLink'
+import { ProjectUpgradeFailedBanner } from 'components/ui/ProjectUpgradeFailedBanner'
+import { useBranchesQuery } from 'data/branches/branches-query'
+import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
+import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
+import { useTablesQuery } from 'data/tables/tables-query'
+import { useCustomContent } from 'hooks/custom-content/useCustomContent'
+import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
+import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
+import {
+ useIsOrioleDb,
+ useProjectByRefQuery,
+ useSelectedProjectQuery,
+} from 'hooks/misc/useSelectedProject'
+import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
+import { useAppStateSnapshot } from 'state/app-state'
+import {
+ Badge,
+ cn,
+ Tabs_Shadcn_,
+ TabsContent_Shadcn_,
+ TabsList_Shadcn_,
+ TabsTrigger_Shadcn_,
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from 'ui'
+import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
+
+export const Home = () => {
+ const { data: project } = useSelectedProjectQuery()
+ const { data: organization } = useSelectedOrganizationQuery()
+ const { data: parentProject } = useProjectByRefQuery(project?.parent_project_ref)
+ const isOrioleDb = useIsOrioleDb()
+ const snap = useAppStateSnapshot()
+ const { ref, enableBranching } = useParams()
+
+ const { projectHomepageExampleProjects } = useCustomContent(['project_homepage:example_projects'])
+
+ const {
+ projectHomepageShowAllClientLibraries: showAllClientLibraries,
+ projectHomepageShowInstanceSize: showInstanceSize,
+ projectHomepageShowExamples: showExamples,
+ } = useIsFeatureEnabled([
+ 'project_homepage:show_all_client_libraries',
+ 'project_homepage:show_instance_size',
+ 'project_homepage:show_examples',
+ ])
+
+ const clientLibraries = useMemo(() => {
+ if (showAllClientLibraries) {
+ return CLIENT_LIBRARIES
+ }
+ return CLIENT_LIBRARIES.filter((library) => library.language === 'JavaScript')
+ }, [showAllClientLibraries])
+
+ const hasShownEnableBranchingModalRef = useRef(false)
+ const isPaused = project?.status === PROJECT_STATUS.INACTIVE
+ const isNewProject = dayjs(project?.inserted_at).isAfter(dayjs().subtract(2, 'day'))
+
+ useEffect(() => {
+ if (enableBranching && !hasShownEnableBranchingModalRef.current) {
+ hasShownEnableBranchingModalRef.current = true
+ snap.setShowCreateBranchModal(true)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [enableBranching])
+
+ const { data: tablesData, isLoading: isLoadingTables } = useTablesQuery({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ schema: 'public',
+ })
+ const { data: functionsData, isLoading: isLoadingFunctions } = useEdgeFunctionsQuery({
+ projectRef: project?.ref,
+ })
+ const { data: replicasData, isLoading: isLoadingReplicas } = useReadReplicasQuery({
+ projectRef: project?.ref,
+ })
+
+ const { data: branches } = useBranchesQuery({
+ projectRef: project?.parent_project_ref ?? project?.ref,
+ })
+
+ const mainBranch = branches?.find((branch) => branch.is_default)
+ const currentBranch = branches?.find((branch) => branch.project_ref === project?.ref)
+ const isMainBranch = currentBranch?.name === mainBranch?.name
+ let projectName = 'Welcome to your project'
+
+ if (currentBranch && !isMainBranch) {
+ projectName = currentBranch?.name
+ } else if (project?.name) {
+ projectName = project?.name
+ }
+
+ const tablesCount = Math.max(0, tablesData?.length ?? 0)
+ const functionsCount = Math.max(0, functionsData?.length ?? 0)
+ // [Joshen] JFYI minus 1 as the replicas endpoint returns the primary DB minimally
+ const replicasCount = Math.max(0, (replicasData?.length ?? 1) - 1)
+
+ return (
+
+
+
+
+
+
+ {!isMainBranch && (
+
+ {parentProject?.name}
+
+ )}
+
{projectName}
+
+
+ {isOrioleDb && (
+
+
+ OrioleDB
+
+
+ This project is using Postgres with OrioleDB which is currently in preview and
+ not suitable for production workloads. View our{' '}
+
+ documentation
+ {' '}
+ for all limitations.
+
+
+ )}
+ {showInstanceSize && (
+
+ )}
+
+
+
+ {project?.status === PROJECT_STATUS.ACTIVE_HEALTHY && (
+
+
+
+ Tables
+
+
+ {isLoadingTables ? (
+
+ ) : (
+
{tablesCount}
+ )}
+
+
+ {IS_PLATFORM && (
+
+
+ Functions
+
+ {isLoadingFunctions ? (
+
+ ) : (
+
{functionsCount}
+ )}
+
+ )}
+
+ {IS_PLATFORM && (
+
+
+ Replicas
+
+ {isLoadingReplicas ? (
+
+ ) : (
+
{replicasCount}
+ )}
+
+ )}
+
+ )}
+ {IS_PLATFORM && project?.status === PROJECT_STATUS.ACTIVE_HEALTHY && (
+
+
+
+ )}
+
+
+
+ {isPaused &&
}
+
+
+
+ {!isPaused && (
+ <>
+
+
+ {IS_PLATFORM && project?.status !== PROJECT_STATUS.INACTIVE && (
+ <>{isNewProject ?
:
}>
+ )}
+ {!isNewProject && project?.status !== PROJECT_STATUS.INACTIVE &&
}
+
+
+
+
+
+ {project?.status !== PROJECT_STATUS.INACTIVE && (
+ <>
+
+
Client libraries
+
+ {clientLibraries.map((library) => (
+
+ ))}
+
+
+ {showExamples && (
+
+
Example projects
+ {!!projectHomepageExampleProjects ? (
+
+ {projectHomepageExampleProjects
+ .sort((a, b) => a.title.localeCompare(b.title))
+ .map((project) => (
+
+ ))}
+
+ ) : (
+
+
+
+ App Frameworks
+
+ Mobile Frameworks
+
+
+
+
+ {EXAMPLE_PROJECTS.filter((project) => project.type === 'app')
+ .sort((a, b) => a.title.localeCompare(b.title))
+ .map((project) => (
+
+ ))}
+
+
+
+
+ {EXAMPLE_PROJECTS.filter((project) => project.type === 'mobile')
+ .sort((a, b) => a.title.localeCompare(b.title))
+ .map((project) => (
+
+ ))}
+
+
+
+
+ )}
+
+ )}
+ >
+ )}
+
+
+ >
+ )}
+
+ )
+}
diff --git a/apps/studio/components/interfaces/HomeNew/ActivityStats.tsx b/apps/studio/components/interfaces/HomeNew/ActivityStats.tsx
new file mode 100644
index 0000000000000..22464a7a5fbc3
--- /dev/null
+++ b/apps/studio/components/interfaces/HomeNew/ActivityStats.tsx
@@ -0,0 +1,131 @@
+import dayjs from 'dayjs'
+import { GitBranch } from 'lucide-react'
+import Link from 'next/link'
+import { useMemo } from 'react'
+
+import { useParams } from 'common'
+import { useBranchesQuery } from 'data/branches/branches-query'
+import { useBackupsQuery } from 'data/database/backups-query'
+import { useMigrationsQuery } from 'data/database/migrations-query'
+import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
+import { cn, Skeleton } from 'ui'
+import { TimestampInfo } from 'ui-patterns'
+import { ServiceStatus } from './ServiceStatus'
+
+export const ActivityStats = () => {
+ const { ref } = useParams()
+ const { data: project } = useSelectedProjectQuery()
+
+ const { data: branchesData, isLoading: isLoadingBranches } = useBranchesQuery({
+ projectRef: project?.parent_project_ref ?? project?.ref,
+ })
+ const isDefaultProject = project?.parent_project_ref === undefined
+ const currentBranch = useMemo(
+ () => (branchesData ?? []).find((b) => b.project_ref === ref),
+ [branchesData, ref]
+ )
+ const latestNonDefaultBranch = useMemo(() => {
+ const list = (branchesData ?? []).filter((b) => !b.is_default)
+ if (list.length === 0) return undefined
+ return list
+ .slice()
+ .sort(
+ (a, b) =>
+ new Date(b.created_at ?? b.updated_at).valueOf() -
+ new Date(a.created_at ?? a.updated_at).valueOf()
+ )[0]
+ }, [branchesData])
+
+ const { data: migrationsData, isLoading: isLoadingMigrations } = useMigrationsQuery({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ })
+ const latestMigration = useMemo(() => (migrationsData ?? [])[0], [migrationsData])
+
+ const { data: backupsData, isLoading: isLoadingBackups } = useBackupsQuery({
+ projectRef: project?.ref,
+ })
+ const latestBackup = useMemo(() => {
+ const list = backupsData?.backups ?? []
+ if (list.length === 0) return undefined
+ return list
+ .slice()
+ .sort((a, b) => new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf())[0]
+ }, [backupsData])
+
+ return (
+
+
+
+
+
+
Last migration
+
+
+ {isLoadingMigrations ? (
+
+ ) : latestMigration ? (
+
+ ) : (
+
No migrations
+ )}
+
+
+
+
+
Last backup
+
+
+ {isLoadingBackups ? (
+
+ ) : backupsData?.pitr_enabled ? (
+
PITR enabled
+ ) : latestBackup ? (
+
+ ) : (
+
No backups
+ )}
+
+
+
+
+
+ {isDefaultProject ? 'Recent branch' : 'Branch Created'}
+
+
+
+ {isLoadingBranches ? (
+
+ ) : isDefaultProject ? (
+
+
+
+ {latestNonDefaultBranch?.name ?? 'No branches'}
+
+
+ ) : currentBranch?.created_at ? (
+
+ ) : (
+
Unknown
+ )}
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/HomeNew/Home.tsx b/apps/studio/components/interfaces/HomeNew/Home.tsx
new file mode 100644
index 0000000000000..5c2390d1c473f
--- /dev/null
+++ b/apps/studio/components/interfaces/HomeNew/Home.tsx
@@ -0,0 +1,115 @@
+import { DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'
+import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
+import { useEffect, useRef } from 'react'
+
+import { useParams } from 'common'
+import { SortableSection } from 'components/interfaces/HomeNew/SortableSection'
+import { TopSection } from 'components/interfaces/HomeNew/TopSection'
+import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
+import { useBranchesQuery } from 'data/branches/branches-query'
+import { useLocalStorage } from 'hooks/misc/useLocalStorage'
+import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
+import {
+ useIsOrioleDb,
+ useProjectByRefQuery,
+ useSelectedProjectQuery,
+} from 'hooks/misc/useSelectedProject'
+import { PROJECT_STATUS } from 'lib/constants'
+import { useAppStateSnapshot } from 'state/app-state'
+
+export const HomeV2 = () => {
+ const { ref, enableBranching } = useParams()
+ const isOrioleDb = useIsOrioleDb()
+ const snap = useAppStateSnapshot()
+ const { data: project } = useSelectedProjectQuery()
+ const { data: organization } = useSelectedOrganizationQuery()
+ const { data: parentProject } = useProjectByRefQuery(project?.parent_project_ref)
+
+ const hasShownEnableBranchingModalRef = useRef(false)
+ const isPaused = project?.status === PROJECT_STATUS.INACTIVE
+
+ const { data: branches } = useBranchesQuery({
+ projectRef: project?.parent_project_ref ?? project?.ref,
+ })
+
+ const mainBranch = branches?.find((branch) => branch.is_default)
+ const currentBranch = branches?.find((branch) => branch.project_ref === project?.ref)
+ const isMainBranch = currentBranch?.name === mainBranch?.name
+
+ const projectName =
+ currentBranch && !isMainBranch
+ ? currentBranch.name
+ : project?.name
+ ? project.name
+ : 'Welcome to your project'
+
+ const [sectionOrder, setSectionOrder] = useLocalStorage(
+ `home-section-order-${project?.ref || 'default'}`,
+ ['getting-started', 'usage', 'advisor', 'custom-report']
+ )
+
+ const [gettingStartedState] = useLocalStorage<'empty' | 'code' | 'no-code' | 'hidden'>(
+ `home-getting-started-${project?.ref || 'default'}`,
+ 'empty'
+ )
+
+ const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }))
+
+ const handleDragEnd = (event: DragEndEvent) => {
+ const { active, over } = event
+ if (!over || active.id === over.id) return
+ setSectionOrder((items) => {
+ const oldIndex = items.indexOf(String(active.id))
+ const newIndex = items.indexOf(String(over.id))
+ if (oldIndex === -1 || newIndex === -1) return items
+ return arrayMove(items, oldIndex, newIndex)
+ })
+ }
+
+ useEffect(() => {
+ if (enableBranching && !hasShownEnableBranchingModalRef.current) {
+ hasShownEnableBranchingModalRef.current = true
+ snap.setShowCreateBranchModal(true)
+ }
+ }, [enableBranching, snap])
+
+ return (
+
+
+
+
+
+
+
+ {!isPaused && (
+
+
+
+ id !== 'getting-started' || gettingStartedState !== 'hidden'
+ )}
+ strategy={verticalListSortingStrategy}
+ >
+ {sectionOrder.map((id) => (
+
+ {id}
+
+ ))}
+
+
+
+
+ )}
+
+ )
+}
diff --git a/apps/studio/components/interfaces/HomeNew/ServiceStatus.tsx b/apps/studio/components/interfaces/HomeNew/ServiceStatus.tsx
new file mode 100644
index 0000000000000..768c0de480a36
--- /dev/null
+++ b/apps/studio/components/interfaces/HomeNew/ServiceStatus.tsx
@@ -0,0 +1,331 @@
+import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Loader2 } from 'lucide-react'
+import Link from 'next/link'
+import { useState } from 'react'
+
+import { PopoverSeparator } from '@ui/components/shadcn/ui/popover'
+import { useParams } from 'common'
+import { useBranchesQuery } from 'data/branches/branches-query'
+import { useEdgeFunctionServiceStatusQuery } from 'data/service-status/edge-functions-status-query'
+import {
+ ProjectServiceStatus,
+ useProjectServiceStatusQuery,
+} from 'data/service-status/service-status-query'
+import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
+import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
+import {
+ Button,
+ InfoIcon,
+ PopoverContent_Shadcn_,
+ PopoverTrigger_Shadcn_,
+ Popover_Shadcn_,
+ cn,
+} from 'ui'
+
+/**
+ * [Joshen] JFYI before we go live with this, we need to revisit the migrations section
+ * as I don't think it should live in the ServiceStatus component since its not indicative
+ * of a project's "service". ServiceStatus's intention is to be an ongoing health/status check.
+ *
+ * For context, migrations are meant to be indicative for only when creating branches or projects
+ * with an initial SQL, so "healthy" migrations just means that migrations have all been successfully
+ * ran. So it might be a matter of decoupling "ready" state vs "health checks"
+ * [Edit] Now that migrations are only showing up if the project is a branch, i think its okay for now
+ *
+ * [Joshen] Another issue that requires investigation before we go live with the changes:
+ * We've removed the isProjectNew check in this component which we had that logic cause new
+ * projects would show unhealthy as the services are still starting up - but it causes a
+ * perceived negative impression as new projects were showing unhealthy, hence the 5 minute
+ * threshold check (we’d show “Coming up” instead of “unhealthy” if the project is within 5
+ * minutes of when it was created). Might be related to decoupling "ready" state vs "health checks"
+ */
+
+const StatusMessage = ({
+ status,
+ isLoading,
+ isHealthy,
+}: {
+ isLoading: boolean
+ isHealthy: boolean
+ status?: ProjectServiceStatus
+}) => {
+ if (isHealthy) return 'Healthy'
+ if (isLoading) return 'Checking status'
+ if (status === 'UNHEALTHY') return 'Unhealthy'
+ if (status === 'COMING_UP') return 'Coming up...'
+ if (status === 'ACTIVE_HEALTHY') return 'Healthy'
+ if (status) return status
+ return 'Unable to connect'
+}
+
+const iconProps = {
+ size: 18,
+ strokeWidth: 1.5,
+}
+const LoaderIcon = () =>
+const AlertIcon = () =>
+const CheckIcon = () =>
+
+const StatusIcon = ({
+ isLoading,
+ isHealthy,
+ projectStatus,
+}: {
+ isLoading: boolean
+ isHealthy: boolean
+ projectStatus?: ProjectServiceStatus
+}) => {
+ if (isHealthy) return
+ if (isLoading) return
+ if (projectStatus === 'UNHEALTHY') return
+ if (projectStatus === 'COMING_UP') return
+ if (projectStatus === 'ACTIVE_HEALTHY') return
+ return
+}
+
+export const ServiceStatus = () => {
+ const { ref } = useParams()
+ const { data: project } = useSelectedProjectQuery()
+ const [open, setOpen] = useState(false)
+
+ const {
+ projectAuthAll: authEnabled,
+ projectEdgeFunctionAll: edgeFunctionsEnabled,
+ realtimeAll: realtimeEnabled,
+ projectStorageAll: storageEnabled,
+ } = useIsFeatureEnabled([
+ 'project_auth:all',
+ 'project_edge_function:all',
+ 'realtime:all',
+ 'project_storage:all',
+ ])
+
+ const isBranch = project?.parentRef !== project?.ref
+
+ // Get branches data when on a branch
+ const { data: branches, isLoading: isBranchesLoading } = useBranchesQuery(
+ { projectRef: isBranch ? project?.parentRef : undefined },
+ {
+ enabled: isBranch,
+ }
+ )
+
+ const currentBranch = isBranch
+ ? branches?.find((branch) => branch.project_ref === ref)
+ : undefined
+
+ // [Joshen] Need pooler service check eventually
+ const { data: status, isLoading } = useProjectServiceStatusQuery(
+ { projectRef: ref },
+ { refetchInterval: (data) => (data?.some((service) => !service.healthy) ? 5000 : false) }
+ )
+ const { data: edgeFunctionsStatus } = useEdgeFunctionServiceStatusQuery(
+ { projectRef: ref },
+ { refetchInterval: (data) => (!data?.healthy ? 5000 : false) }
+ )
+
+ const authStatus = status?.find((service) => service.name === 'auth')
+ const restStatus = status?.find((service) => service.name === 'rest')
+ const realtimeStatus = status?.find((service) => service.name === 'realtime')
+ const storageStatus = status?.find((service) => service.name === 'storage')
+ const dbStatus = status?.find((service) => service.name === 'db')
+
+ const isMigrationLoading =
+ project?.status === 'COMING_UP' ||
+ (isBranch &&
+ (isBranchesLoading ||
+ currentBranch?.status === 'CREATING_PROJECT' ||
+ currentBranch?.status === 'RUNNING_MIGRATIONS'))
+
+ // [Joshen] Need individual troubleshooting docs for each service eventually for users to self serve
+ const services: {
+ name: string
+ error?: string
+ docsUrl?: string
+ isLoading: boolean
+ isHealthy: boolean
+ status: ProjectServiceStatus
+ logsUrl: string
+ }[] = [
+ {
+ name: 'Database',
+ error: undefined,
+ docsUrl: undefined,
+ isLoading: isLoading,
+ isHealthy: !!dbStatus?.healthy,
+ status: dbStatus?.status ?? 'UNHEALTHY',
+ logsUrl: '/logs/postgres-logs',
+ },
+ {
+ name: 'PostgREST',
+ error: restStatus?.error,
+ docsUrl: undefined,
+ isLoading,
+ isHealthy: !!restStatus?.healthy,
+ status: restStatus?.status ?? 'UNHEALTHY',
+ logsUrl: '/logs/postgrest-logs',
+ },
+ ...(authEnabled
+ ? [
+ {
+ name: 'Auth',
+ error: authStatus?.error,
+ docsUrl: undefined,
+ isLoading,
+ isHealthy: !!authStatus?.healthy,
+ status: authStatus?.status ?? 'UNHEALTHY',
+ logsUrl: '/logs/auth-logs',
+ },
+ ]
+ : []),
+ ...(realtimeEnabled
+ ? [
+ {
+ name: 'Realtime',
+ error: realtimeStatus?.error,
+ docsUrl: undefined,
+ isLoading,
+ isHealthy: !!realtimeStatus?.healthy,
+ status: realtimeStatus?.status ?? 'UNHEALTHY',
+ logsUrl: '/logs/realtime-logs',
+ },
+ ]
+ : []),
+ ...(storageEnabled
+ ? [
+ {
+ name: 'Storage',
+ error: storageStatus?.error,
+ docsUrl: undefined,
+ isLoading,
+ isHealthy: !!storageStatus?.healthy,
+ status: storageStatus?.status ?? 'UNHEALTHY',
+ logsUrl: '/logs/storage-logs',
+ },
+ ]
+ : []),
+ ...(edgeFunctionsEnabled
+ ? [
+ {
+ name: 'Edge Functions',
+ error: undefined,
+ docsUrl: 'https://supabase.com/docs/guides/functions/troubleshooting',
+ isLoading,
+ isHealthy: !!edgeFunctionsStatus?.healthy,
+ status: edgeFunctionsStatus?.healthy
+ ? 'ACTIVE_HEALTHY'
+ : isLoading
+ ? 'COMING_UP'
+ : ('UNHEALTHY' as ProjectServiceStatus),
+ logsUrl: '/logs/edge-functions-logs',
+ },
+ ]
+ : []),
+ ...(isBranch
+ ? [
+ {
+ name: 'Migrations',
+ error: undefined,
+ docsUrl: undefined,
+ isLoading: isBranchesLoading,
+ isHealthy: isBranch
+ ? currentBranch?.status === 'FUNCTIONS_DEPLOYED'
+ : !isMigrationLoading,
+ status: (isBranch
+ ? currentBranch?.status === 'FUNCTIONS_DEPLOYED'
+ ? 'ACTIVE_HEALTHY'
+ : currentBranch?.status === 'FUNCTIONS_FAILED' ||
+ currentBranch?.status === 'MIGRATIONS_FAILED'
+ ? 'UNHEALTHY'
+ : 'COMING_UP'
+ : isMigrationLoading
+ ? 'COMING_UP'
+ : 'ACTIVE_HEALTHY') as ProjectServiceStatus,
+ logsUrl: isBranch ? '/branches' : '/logs/database-logs',
+ },
+ ]
+ : []),
+ ]
+
+ const isLoadingChecks = services.some((service) => service.isLoading)
+ const allServicesOperational = services.every((service) => service.isHealthy)
+
+ const anyUnhealthy = services.some(
+ (service) => !service.isHealthy && service.status !== 'COMING_UP'
+ )
+ const anyComingUp = services.some((service) => service.status === 'COMING_UP')
+ const overallStatusLabel = isLoadingChecks
+ ? 'Checking...'
+ : anyUnhealthy
+ ? 'Unhealthy'
+ : anyComingUp || isMigrationLoading
+ ? 'Coming up...'
+ : 'Healthy'
+
+ return (
+
+
+ }
+ icon={
+ isLoadingChecks || anyComingUp || isMigrationLoading ? (
+
+ ) : (
+
+ )
+ }
+ >
+ {overallStatusLabel}
+
+
+
+ {services.map((service) => (
+
+
+
+ View logs
+
+
+
+ ))}
+ {allServicesOperational ? null : (
+ <>
+
+
+
+
+
+ Recently restored projects can take up to 5 minutes to become fully operational.
+
+ >
+ )}
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/HomeNew/SortableSection.tsx b/apps/studio/components/interfaces/HomeNew/SortableSection.tsx
new file mode 100644
index 0000000000000..78704e042da48
--- /dev/null
+++ b/apps/studio/components/interfaces/HomeNew/SortableSection.tsx
@@ -0,0 +1,36 @@
+import { useSortable } from '@dnd-kit/sortable'
+import { GripVertical } from 'lucide-react'
+import type { CSSProperties, ReactNode } from 'react'
+import { cn } from 'ui'
+
+type SortableSectionProps = {
+ id: string
+ children: ReactNode
+}
+
+export const SortableSection = ({ id, children }: SortableSectionProps) => {
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
+ id,
+ })
+
+ const style: CSSProperties = {
+ transform: transform
+ ? `translate3d(${Math.round(transform.x)}px, ${Math.round(transform.y)}px, 0)`
+ : undefined,
+ transition,
+ }
+
+ return (
+
+ )
+}
diff --git a/apps/studio/components/interfaces/HomeNew/TopSection.tsx b/apps/studio/components/interfaces/HomeNew/TopSection.tsx
new file mode 100644
index 0000000000000..56c026709ebb2
--- /dev/null
+++ b/apps/studio/components/interfaces/HomeNew/TopSection.tsx
@@ -0,0 +1,94 @@
+import Link from 'next/link'
+
+import { ActivityStats } from 'components/interfaces/HomeNew/ActivityStats'
+import { ProjectPausedState } from 'components/layouts/ProjectLayout/PausedState/ProjectPausedState'
+import { ComputeBadgeWrapper } from 'components/ui/ComputeBadgeWrapper'
+import { InlineLink } from 'components/ui/InlineLink'
+import { ProjectUpgradeFailedBanner } from 'components/ui/ProjectUpgradeFailedBanner'
+import { ReactFlowProvider } from 'reactflow'
+import { Badge, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
+import { InstanceConfiguration } from '../Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration'
+
+interface TopSectionProps {
+ projectName: string
+ isMainBranch?: boolean
+ parentProject?: { ref?: string; name?: string } | null
+ isOrioleDb?: boolean
+ project: any
+ organization: any
+ projectRef?: string
+ isPaused: boolean
+}
+
+export const TopSection = ({
+ projectName,
+ isMainBranch,
+ parentProject,
+ isOrioleDb,
+ project,
+ organization,
+ isPaused,
+}: TopSectionProps) => {
+ return (
+
+
+
+
+
+ {!isMainBranch && (
+
+ {parentProject?.name}
+
+ )}
+
{projectName}
+
+
+ {isOrioleDb && (
+
+
+ OrioleDB
+
+
+ This project is using Postgres with OrioleDB which is currently in preview and
+ not suitable for production workloads. View our{' '}
+
+ documentation
+ {' '}
+ for all limitations.
+
+
+ )}
+
+
+
+
+
+
+
+
+ {isPaused &&
}
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx
index 54d50a87d7778..c52f590d526f2 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx
@@ -61,6 +61,7 @@ export const BillingCustomerData = () => {
line1: customerProfile?.address?.line1,
line2: customerProfile?.address?.line2 ?? undefined,
postal_code: customerProfile?.address?.postal_code ?? undefined,
+ state: customerProfile?.address?.state ?? undefined,
billing_name: customerProfile?.billing_name,
tax_id_type: taxId?.type,
tax_id_value: taxId?.value,
diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx
index ea3cee853fcb0..f74301cc8c6ab 100644
--- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx
+++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx
@@ -1,12 +1,13 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { useParams } from 'common'
import { partition } from 'lodash'
import { ChevronDown, Globe2, Loader2, Network } from 'lucide-react'
import { useTheme } from 'next-themes'
+import Link from 'next/link'
import { useEffect, useMemo, useRef, useState } from 'react'
import ReactFlow, { Background, Edge, ReactFlowProvider, useReactFlow } from 'reactflow'
import 'reactflow/dist/style.css'
+import { useParams } from 'common'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useLoadBalancersQuery } from 'data/read-replicas/load-balancers-query'
@@ -18,7 +19,6 @@ import {
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsOrioleDb, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { timeout } from 'lib/helpers'
-import Link from 'next/link'
import { type AWS_REGIONS_KEYS } from 'shared-data'
import {
Button,
@@ -40,7 +40,11 @@ import MapView from './MapView'
import { RestartReplicaConfirmationModal } from './RestartReplicaConfirmationModal'
import { useShowNewReplicaPanel } from './use-show-new-replica'
-const InstanceConfigurationUI = () => {
+interface InstanceConfigurationUIProps {
+ diagramOnly?: boolean
+}
+
+const InstanceConfigurationUI = ({ diagramOnly = false }: InstanceConfigurationUIProps) => {
const reactFlow = useReactFlow()
const isOrioleDb = useIsOrioleDb()
const { resolvedTheme } = useTheme()
@@ -210,9 +214,9 @@ const InstanceConfigurationUI = () => {
}, [isSuccessReplicas, isSuccessLoadBalancers, nodes, edges, view])
return (
-
+
@@ -224,70 +228,72 @@ const InstanceConfigurationUI = () => {
{isError &&
}
{isSuccessReplicas && !isLoadingProject && (
<>
-
-
-
0 ? 'rounded-r-none' : '')}
- onClick={() => setShowNewReplicaPanel(true)}
- tooltip={{
- content: {
- side: 'bottom',
- text: !canManageReplicas
- ? 'You need additional permissions to deploy replicas'
- : isOrioleDb
- ? 'Read replicas are not supported with OrioleDB'
- : undefined,
- },
- }}
- >
- Deploy a new replica
-
- {replicas.length > 0 && (
-
-
- }
- className="px-1 rounded-l-none border-l-0"
- />
-
-
-
-
- Resize databases
-
-
-
- setShowDeleteAllModal(true)}>
- Remove all replicas
-
-
-
- )}
-
- {project?.cloud_provider === 'AWS' && (
+ {!diagramOnly && (
+
-
}
- className={`rounded-r-none transition ${
- view === 'flow' ? 'opacity-100' : 'opacity-50'
- }`}
- onClick={() => setView('flow')}
- />
-
}
- className={`rounded-l-none transition ${
- view === 'map' ? 'opacity-100' : 'opacity-50'
- }`}
- onClick={() => setView('map')}
- />
+ disabled={!canManageReplicas || isOrioleDb}
+ className={cn(replicas.length > 0 ? 'rounded-r-none' : '')}
+ onClick={() => setShowNewReplicaPanel(true)}
+ tooltip={{
+ content: {
+ side: 'bottom',
+ text: !canManageReplicas
+ ? 'You need additional permissions to deploy replicas'
+ : isOrioleDb
+ ? 'Read replicas are not supported with OrioleDB'
+ : undefined,
+ },
+ }}
+ >
+ Deploy a new replica
+
+ {replicas.length > 0 && (
+
+
+ }
+ className="px-1 rounded-l-none border-l-0"
+ />
+
+
+
+
+ Resize databases
+
+
+
+ setShowDeleteAllModal(true)}>
+ Remove all replicas
+
+
+
+ )}
- )}
-
+ {project?.cloud_provider === 'AWS' && (
+
+ }
+ className={`rounded-r-none transition ${
+ view === 'flow' ? 'opacity-100' : 'opacity-50'
+ }`}
+ onClick={() => setView('flow')}
+ />
+ }
+ className={`rounded-l-none transition ${
+ view === 'map' ? 'opacity-100' : 'opacity-50'
+ }`}
+ onClick={() => setView('map')}
+ />
+
+ )}
+
+ )}
{view === 'flow' ? (
{
)}
-
setRefetchInterval(5000)}
- onClose={() => {
- setNewReplicaRegion(undefined)
- setShowNewReplicaPanel(false)
- }}
- />
+ {!diagramOnly && (
+ <>
+ setRefetchInterval(5000)}
+ onClose={() => {
+ setNewReplicaRegion(undefined)
+ setShowNewReplicaPanel(false)
+ }}
+ />
- setRefetchInterval(5000)}
- onCancel={() => setSelectedReplicaToDrop(undefined)}
- />
+ setRefetchInterval(5000)}
+ onCancel={() => setSelectedReplicaToDrop(undefined)}
+ />
- setRefetchInterval(5000)}
- onCancel={() => setShowDeleteAllModal(false)}
- />
+ setRefetchInterval(5000)}
+ onCancel={() => setShowDeleteAllModal(false)}
+ />
- setRefetchInterval(5000)}
- onCancel={() => setSelectedReplicaToRestart(undefined)}
- />
+ setRefetchInterval(5000)}
+ onCancel={() => setSelectedReplicaToRestart(undefined)}
+ />
+ >
+ )}
)
}
-const InstanceConfiguration = () => {
+interface InstanceConfigurationProps {
+ diagramOnly?: boolean
+}
+
+export const InstanceConfiguration = ({ diagramOnly = false }: InstanceConfigurationProps) => {
return (
-
+
)
}
-
-export default InstanceConfiguration
diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx
index c2e375ade9bf2..88c9a63eba5dc 100644
--- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx
+++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx
@@ -24,7 +24,7 @@ import {
TooltipTrigger,
} from 'ui'
import { ProjectUpgradeAlert } from '../General/Infrastructure/ProjectUpgradeAlert'
-import InstanceConfiguration from './InfrastructureConfiguration/InstanceConfiguration'
+import { InstanceConfiguration } from './InfrastructureConfiguration/InstanceConfiguration'
import {
ObjectsToBeDroppedWarning,
ReadReplicasWarning,
diff --git a/apps/studio/pages/project/[ref]/index.tsx b/apps/studio/pages/project/[ref]/index.tsx
index 567ecaa180002..b786f450fe37b 100644
--- a/apps/studio/pages/project/[ref]/index.tsx
+++ b/apps/studio/pages/project/[ref]/index.tsx
@@ -1,308 +1,22 @@
-import dayjs from 'dayjs'
-import Link from 'next/link'
-import { useEffect, useMemo, useRef } from 'react'
-
-import { useParams } from 'common'
-import { ClientLibrary } from 'components/interfaces/Home'
-import { AdvisorWidget } from 'components/interfaces/Home/AdvisorWidget'
-import { ExampleProject } from 'components/interfaces/Home/ExampleProject'
-import { CLIENT_LIBRARIES, EXAMPLE_PROJECTS } from 'components/interfaces/Home/Home.constants'
-import { NewProjectPanel } from 'components/interfaces/Home/NewProjectPanel/NewProjectPanel'
-import { ProjectUsageSection } from 'components/interfaces/Home/ProjectUsageSection'
-import { ServiceStatus } from 'components/interfaces/Home/ServiceStatus'
+import { useFlag } from 'common'
+import { Home } from 'components/interfaces/Home/Home'
+import { HomeV2 } from 'components/interfaces/HomeNew/Home'
import DefaultLayout from 'components/layouts/DefaultLayout'
-import { ProjectPausedState } from 'components/layouts/ProjectLayout/PausedState/ProjectPausedState'
import { ProjectLayoutWithAuth } from 'components/layouts/ProjectLayout/ProjectLayout'
-import { ComputeBadgeWrapper } from 'components/ui/ComputeBadgeWrapper'
-import { InlineLink } from 'components/ui/InlineLink'
-import { ProjectUpgradeFailedBanner } from 'components/ui/ProjectUpgradeFailedBanner'
-import { useBranchesQuery } from 'data/branches/branches-query'
-import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
-import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
-import { useTablesQuery } from 'data/tables/tables-query'
-import { useCustomContent } from 'hooks/custom-content/useCustomContent'
-import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
-import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
-import {
- useIsOrioleDb,
- useProjectByRefQuery,
- useSelectedProjectQuery,
-} from 'hooks/misc/useSelectedProject'
-import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
-import { useAppStateSnapshot } from 'state/app-state'
import type { NextPageWithLayout } from 'types'
-import {
- Badge,
- cn,
- Tabs_Shadcn_,
- TabsContent_Shadcn_,
- TabsList_Shadcn_,
- TabsTrigger_Shadcn_,
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from 'ui'
-import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
-
-const Home: NextPageWithLayout = () => {
- const { data: project } = useSelectedProjectQuery()
- const { data: organization } = useSelectedOrganizationQuery()
- const { data: parentProject } = useProjectByRefQuery(project?.parent_project_ref)
- const isOrioleDb = useIsOrioleDb()
- const snap = useAppStateSnapshot()
- const { ref, enableBranching } = useParams()
-
- const { projectHomepageExampleProjects } = useCustomContent(['project_homepage:example_projects'])
-
- const {
- projectHomepageShowAllClientLibraries: showAllClientLibraries,
- projectHomepageShowInstanceSize: showInstanceSize,
- projectHomepageShowExamples: showExamples,
- } = useIsFeatureEnabled([
- 'project_homepage:show_all_client_libraries',
- 'project_homepage:show_instance_size',
- 'project_homepage:show_examples',
- ])
-
- const clientLibraries = useMemo(() => {
- if (showAllClientLibraries) {
- return CLIENT_LIBRARIES
- }
- return CLIENT_LIBRARIES.filter((library) => library.language === 'JavaScript')
- }, [showAllClientLibraries])
-
- const hasShownEnableBranchingModalRef = useRef(false)
- const isPaused = project?.status === PROJECT_STATUS.INACTIVE
- const isNewProject = dayjs(project?.inserted_at).isAfter(dayjs().subtract(2, 'day'))
-
- useEffect(() => {
- if (enableBranching && !hasShownEnableBranchingModalRef.current) {
- hasShownEnableBranchingModalRef.current = true
- snap.setShowCreateBranchModal(true)
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [enableBranching])
-
- const { data: tablesData, isLoading: isLoadingTables } = useTablesQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- schema: 'public',
- })
- const { data: functionsData, isLoading: isLoadingFunctions } = useEdgeFunctionsQuery({
- projectRef: project?.ref,
- })
- const { data: replicasData, isLoading: isLoadingReplicas } = useReadReplicasQuery({
- projectRef: project?.ref,
- })
-
- const { data: branches } = useBranchesQuery({
- projectRef: project?.parent_project_ref ?? project?.ref,
- })
- const mainBranch = branches?.find((branch) => branch.is_default)
- const currentBranch = branches?.find((branch) => branch.project_ref === project?.ref)
- const isMainBranch = currentBranch?.name === mainBranch?.name
- let projectName = 'Welcome to your project'
-
- if (currentBranch && !isMainBranch) {
- projectName = currentBranch?.name
- } else if (project?.name) {
- projectName = project?.name
+const HomePage: NextPageWithLayout = () => {
+ const isHomeNew = useFlag('homeNew')
+ if (isHomeNew) {
+ return
}
-
- const tablesCount = Math.max(0, tablesData?.length ?? 0)
- const functionsCount = Math.max(0, functionsData?.length ?? 0)
- // [Joshen] JFYI minus 1 as the replicas endpoint returns the primary DB minimally
- const replicasCount = Math.max(0, (replicasData?.length ?? 1) - 1)
-
- return (
-
-
-
-
-
-
- {!isMainBranch && (
-
- {parentProject?.name}
-
- )}
-
{projectName}
-
-
- {isOrioleDb && (
-
-
- OrioleDB
-
-
- This project is using Postgres with OrioleDB which is currently in preview and
- not suitable for production workloads. View our{' '}
-
- documentation
- {' '}
- for all limitations.
-
-
- )}
- {showInstanceSize && (
-
- )}
-
-
-
- {project?.status === PROJECT_STATUS.ACTIVE_HEALTHY && (
-
-
-
- Tables
-
-
- {isLoadingTables ? (
-
- ) : (
-
{tablesCount}
- )}
-
-
- {IS_PLATFORM && (
-
-
- Functions
-
- {isLoadingFunctions ? (
-
- ) : (
-
{functionsCount}
- )}
-
- )}
-
- {IS_PLATFORM && (
-
-
- Replicas
-
- {isLoadingReplicas ? (
-
- ) : (
-
{replicasCount}
- )}
-
- )}
-
- )}
- {IS_PLATFORM && project?.status === PROJECT_STATUS.ACTIVE_HEALTHY && (
-
-
-
- )}
-
-
-
- {isPaused &&
}
-
-
-
- {!isPaused && (
- <>
-
-
- {IS_PLATFORM && project?.status !== PROJECT_STATUS.INACTIVE && (
- <>{isNewProject ?
:
}>
- )}
- {!isNewProject && project?.status !== PROJECT_STATUS.INACTIVE &&
}
-
-
-
-
-
- {project?.status !== PROJECT_STATUS.INACTIVE && (
- <>
-
-
Client libraries
-
- {clientLibraries.map((library) => (
-
- ))}
-
-
- {showExamples && (
-
-
Example projects
- {!!projectHomepageExampleProjects ? (
-
- {projectHomepageExampleProjects
- .sort((a, b) => a.title.localeCompare(b.title))
- .map((project) => (
-
- ))}
-
- ) : (
-
-
-
- App Frameworks
-
- Mobile Frameworks
-
-
-
-
- {EXAMPLE_PROJECTS.filter((project) => project.type === 'app')
- .sort((a, b) => a.title.localeCompare(b.title))
- .map((project) => (
-
- ))}
-
-
-
-
- {EXAMPLE_PROJECTS.filter((project) => project.type === 'mobile')
- .sort((a, b) => a.title.localeCompare(b.title))
- .map((project) => (
-
- ))}
-
-
-
-
- )}
-
- )}
- >
- )}
-
-
- >
- )}
-
- )
+ return
}
-Home.getLayout = (page) => (
+HomePage.getLayout = (page) => (
{page}
)
-export default Home
+export default HomePage
diff --git a/apps/www/components/Sections/TwitterSocialProof.tsx b/apps/www/components/Sections/TwitterSocialProof.tsx
index ec9d6a33eec3c..1c436bdc959fb 100644
--- a/apps/www/components/Sections/TwitterSocialProof.tsx
+++ b/apps/www/components/Sections/TwitterSocialProof.tsx
@@ -4,7 +4,7 @@ import { cn } from 'ui'
import { TweetCard } from 'ui-patterns/TweetCard'
import { range } from 'lib/helpers'
-import Tweets from '~/data/tweets/Tweets.json'
+import tweets from 'shared-data/tweets'
import { useBreakpoint } from 'common'
import React from 'react'
@@ -16,7 +16,7 @@ const TwitterSocialProof: React.FC
= ({ className }) => {
const { basePath } = useRouter()
const isSm = useBreakpoint()
const isMd = useBreakpoint(1024)
- const tweets = Tweets.slice(0, 18)
+ const tweetsData = tweets.slice(0, 18)
return (
<>
@@ -39,7 +39,7 @@ const TwitterSocialProof: React.FC = ({ className }) => {
'will-change-transform transition-transform'
)}
>
- {tweets.slice(0, isSm ? 9 : isMd ? 12 : 18).map((tweet: any, i: number) => (
+ {tweetsData.slice(0, isSm ? 9 : isMd ? 12 : 18).map((tweet: any, i: number) => (
+ {progressBars.map((bar, index) => (
+
+ {/* Background bar (static) */}
+
+
+ {/* Animated foreground bar */}
+
+
+ ))}
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/SurveyChapter.tsx b/apps/www/components/SurveyResults/SurveyChapter.tsx
new file mode 100644
index 0000000000000..dfee436750215
--- /dev/null
+++ b/apps/www/components/SurveyResults/SurveyChapter.tsx
@@ -0,0 +1,71 @@
+import { SurveyPullQuote } from './SurveyPullQuote'
+import './surveyResults.css'
+import { DecorativeProgressBar } from './DecorativeProgressBar'
+
+interface SurveyChapterProps {
+ number: number
+ title: string
+ shortTitle: string
+ description: string
+ pullQuote?: {
+ quote: string
+ author: string
+ authorPosition: string
+ authorAvatar: string
+ }
+ children: React.ReactNode
+}
+
+export function SurveyChapter({
+ number,
+ title,
+ shortTitle,
+ description,
+ pullQuote,
+ children,
+}: SurveyChapterProps) {
+ return (
+
+
+ {/* Chapter header */}
+
+ {/* Decorative progress bar */}
+
+ {/* Text content */}
+
+
+
+ {shortTitle}
+
+
+ {title}
+
+
+
+ {description}
+
+
+
+ {/* Chapter content */}
+
{children}
+
+
+ {pullQuote && (
+
+ )}
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/SurveyChapterSection.tsx b/apps/www/components/SurveyResults/SurveyChapterSection.tsx
new file mode 100644
index 0000000000000..f834a8323d62b
--- /dev/null
+++ b/apps/www/components/SurveyResults/SurveyChapterSection.tsx
@@ -0,0 +1,108 @@
+import { SurveyStatCard } from './SurveyStatCard'
+import { SurveyWordCloud } from './SurveyWordCloud'
+import { SurveySummarizedAnswer } from './SurveySummarizedAnswer'
+import { SurveyRankedAnswersPair } from './SurveyRankedAnswersPair'
+import { SurveySectionBreak } from './SurveySectionBreak'
+import { AcceleratorParticipationChart } from './charts/AcceleratorParticipationChart'
+import { RoleChart } from './charts/RoleChart'
+import { IndustryChart } from './charts/IndustryChart'
+import { FundingStageChart } from './charts/FundingStageChart'
+import { DatabasesChart } from './charts/DatabasesChart'
+import { AIModelsChart } from './charts/AIModelsChart'
+import { LocationChart } from './charts/LocationChart'
+import { SalesToolsChart } from './charts/SalesToolsChart'
+import { AICodingToolsChart } from './charts/AICodingToolsChart'
+import { RegularSocialMediaUseChart } from './charts/RegularSocialMediaUseChart'
+import { NewIdeasChart } from './charts/NewIdeasChart'
+import { InitialPayingCustomersChart } from './charts/InitialPayingCustomersChart'
+import { WorldOutlookChart } from './charts/WorldOutlookChart'
+import { BiggestChallengeChart } from './charts/BiggestChallengeChart'
+
+interface SurveyChapterSectionProps {
+ title: string
+ description: string
+ stats?: Array<{ percent: number; label: string }>
+ charts?: string[]
+
+ wordCloud?: {
+ label: string
+ words: { text: string; count: number }[]
+ }
+ summarizedAnswer?: {
+ label: string
+ answers: string[]
+ }
+ rankedAnswersPair?: Array<{ label: string; answers: string[] }>
+ children?: React.ReactNode
+}
+
+export function SurveyChapterSection({
+ title,
+ description,
+ stats,
+ charts,
+ wordCloud,
+ summarizedAnswer,
+ rankedAnswersPair,
+ children,
+}: SurveyChapterSectionProps) {
+ const chartComponents = {
+ RoleChart,
+ IndustryChart,
+ FundingStageChart,
+ AcceleratorParticipationChart,
+ DatabasesChart,
+ AICodingToolsChart,
+ AIModelsChart,
+ RegularSocialMediaUseChart,
+ NewIdeasChart,
+ InitialPayingCustomersChart,
+ SalesToolsChart,
+ WorldOutlookChart,
+ BiggestChallengeChart,
+ LocationChart,
+ }
+
+ return (
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+ {stats && (
+
+ {stats.map((stat, index) => (
+
+ ))}
+
+ )}
+
+ {charts?.map((chartName, index) => {
+ const ChartComponent = chartComponents[chartName as keyof typeof chartComponents]
+ return ChartComponent ?
: null
+ })}
+
+ {rankedAnswersPair &&
}
+
+ {wordCloud &&
}
+
+ {summarizedAnswer && (
+
+ )}
+
+ {children}
+
+
+
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/SurveyChart.tsx b/apps/www/components/SurveyResults/SurveyChart.tsx
new file mode 100644
index 0000000000000..070a269f98b23
--- /dev/null
+++ b/apps/www/components/SurveyResults/SurveyChart.tsx
@@ -0,0 +1,566 @@
+'use client'
+import { useEffect, useState, useRef, useCallback } from 'react'
+import { createClient } from '@supabase/supabase-js'
+import { motion } from 'framer-motion'
+import {
+ Button,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from 'ui'
+import { ChevronsUpDown } from 'lucide-react'
+import TwoOptionToggle from '../../../studio/components/ui/TwoOptionToggle'
+import CodeBlock from '~/components/CodeBlock/CodeBlock'
+
+// Separate Supabase client for survey project
+const externalSupabase = createClient(
+ process.env.NEXT_PUBLIC_SURVEY_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SURVEY_SUPABASE_ANON_KEY!
+)
+
+// Sentinel for “no filter”
+const NO_FILTER = 'unset'
+
+interface FilterOption {
+ value: string
+ label: string
+}
+
+interface FilterConfig {
+ label: string
+ options: FilterOption[]
+}
+
+interface Filters {
+ [key: string]: FilterConfig
+}
+
+interface ChartDataItem {
+ label: string
+ value: number
+ rawValue: number
+}
+
+interface FilterColumnConfig {
+ label: string
+ options: string[]
+}
+
+// Sometimes the label doesn't match the value, so we need to map them
+// Also, our options aren't always in a predicatable order, so we need to map them too
+const FILTER_COLUMN_CONFIGS: Record = {
+ team_size: {
+ label: 'Team Size',
+ options: ['1–10', '11–50', '51–100', '101–250', '250+'],
+ },
+ money_raised: {
+ label: 'Money Raised',
+ options: ['USD $0–10M', 'USD $11–50M', 'USD $51–100M', 'USD $100M+'],
+ },
+ person_age: {
+ label: 'Age',
+ options: ['18–21', '22–29', '30–39', '40–49', '50–59', '60+'],
+ },
+ location: {
+ label: 'Location',
+ options: [
+ 'Africa',
+ 'Asia',
+ 'Europe',
+ 'Middle East',
+ 'North America',
+ 'South America',
+ 'Remote',
+ ],
+ },
+}
+
+function useFilterOptions(filterColumns: string[]) {
+ // Build filters synchronously since everything is predefined
+ const filters: Filters = {}
+
+ for (const column of filterColumns) {
+ const config = FILTER_COLUMN_CONFIGS[column]
+
+ if (!config) {
+ console.warn(`No configuration found for filter column: ${column}`)
+ continue
+ }
+
+ filters[column] = {
+ label: config.label,
+ options: config.options.map((option) => ({ value: option, label: option })),
+ }
+ }
+
+ return { filters }
+}
+
+// Fetch survey data using secure database functions
+function useSurveyData(
+ shouldFetch: boolean,
+ functionName: string,
+ functionParams: (activeFilters: Record) => Record,
+ activeFilters: Record
+) {
+ const [chartData, setChartData] = useState([])
+ const [isLoading, setIsLoading] = useState(true)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ if (!shouldFetch) return
+
+ async function fetchData() {
+ try {
+ setIsLoading(true)
+ setError(null)
+
+ let data, fetchError
+
+ const functionParamsData = functionParams(activeFilters)
+ const { data: functionData, error: functionError } = await externalSupabase.rpc(
+ functionName,
+ functionParamsData
+ )
+ data = functionData
+ fetchError = functionError
+
+ if (fetchError) {
+ console.error('Error executing SQL query:', fetchError)
+ setError(fetchError.message)
+ return
+ }
+
+ // Calculate total for percentage calculation
+ const total = data.reduce(
+ (sum: number, row: any) => sum + parseInt(row.count || row.total),
+ 0
+ )
+
+ // Transform the data to match chart format
+ const processedData = data.map((row: any) => {
+ const count = parseInt(row.count || row.total)
+ const rawPercentage = total > 0 ? (count / total) * 100 : 0
+ const roundedPercentage = Math.round(rawPercentage)
+
+ return {
+ label: row.label || row.value || row[Object.keys(row)[0]], // Get the first column as label
+ value: roundedPercentage,
+ rawValue: rawPercentage, // Keep the raw value for bar scaling
+ }
+ })
+
+ setChartData(processedData)
+ } catch (err: any) {
+ console.error('Error in fetchData:', err)
+ setError(err.message)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ fetchData()
+ }, [shouldFetch, functionName, functionParams, activeFilters])
+
+ return { chartData, isLoading, error }
+}
+
+interface SurveyChartProps {
+ title: string
+ targetColumn: string
+ filterColumns: string[]
+ generateSQLQuery?: (activeFilters: Record) => string
+ functionName: string
+}
+
+export function SurveyChart({
+ title,
+ targetColumn, // Used for SQL query generation when generateSQLQuery is provided
+ filterColumns,
+ generateSQLQuery,
+ functionName,
+}: SurveyChartProps) {
+ const [isInView, setIsInView] = useState(false)
+ const chartRef = useRef(null)
+ const [shouldAnimateBars, setShouldAnimateBars] = useState(false)
+ const [hasLoadedOnce, setHasLoadedOnce] = useState(false)
+
+ // Intersection observer to trigger chart data loading via database function
+ useEffect(() => {
+ const chartRefCurrent = chartRef.current
+
+ const observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting && !hasLoadedOnce) {
+ setIsInView(true)
+ setHasLoadedOnce(true)
+ observer.disconnect() // Only trigger once
+ }
+ })
+ },
+ {
+ threshold: 0.1,
+ rootMargin: '128px', // Start loading before the component comes into view
+ }
+ )
+
+ if (chartRefCurrent) {
+ observer.observe(chartRefCurrent)
+ }
+
+ return () => {
+ if (chartRefCurrent) {
+ observer.unobserve(chartRefCurrent)
+ }
+ }
+ }, [hasLoadedOnce])
+
+ // Each chart uses a subset of available filters, defined below
+ const { filters } = useFilterOptions(filterColumns)
+
+ // Start with all filters unset (showing "all")
+ const [activeFilters, setActiveFilters] = useState(
+ filterColumns.reduce(
+ (acc: Record, col: string) => ({ ...acc, [col]: NO_FILTER }),
+ {}
+ )
+ )
+
+ // Build function parameters based on filterColumns
+ const buildFunctionParams = useCallback((activeFilters: Record) => {
+ const params: Record = {}
+
+ // Convert single values to arrays for the function parameters
+ if (activeFilters.person_age && activeFilters.person_age !== 'unset') {
+ params.person_age_filter = [activeFilters.person_age]
+ }
+ if (activeFilters.location && activeFilters.location !== 'unset') {
+ params.location_filter = [activeFilters.location]
+ }
+ if (activeFilters.money_raised && activeFilters.money_raised !== 'unset') {
+ params.money_raised_filter = [activeFilters.money_raised]
+ }
+ if (activeFilters.team_size && activeFilters.team_size !== 'unset') {
+ params.team_size_filter = [activeFilters.team_size]
+ }
+
+ return params
+ }, [])
+
+ // Use the custom hook to fetch data
+ const {
+ chartData,
+ isLoading: dataLoading,
+ error: dataError,
+ } = useSurveyData(isInView, functionName, buildFunctionParams, activeFilters)
+
+ // Reset animation state when filters change
+ useEffect(() => {
+ setShouldAnimateBars(false)
+ }, [activeFilters])
+
+ // Trigger bar animation when data loads
+ useEffect(() => {
+ if (!dataLoading && chartData.length > 0 && !shouldAnimateBars) {
+ // Small delay to ensure DOM is ready
+ const timer = setTimeout(() => {
+ setShouldAnimateBars(true)
+ }, 100)
+
+ return () => clearTimeout(timer)
+ }
+ }, [dataLoading, chartData.length, shouldAnimateBars])
+
+ const [view, setView] = useState<'chart' | 'sql'>('chart')
+
+ // Handle both view change and expansion via a wrapper function
+ const handleViewChange = (newView: 'chart' | 'sql') => {
+ setView(newView)
+ setIsExpanded(true)
+ }
+
+ const setFilterValue = (filterKey: string, value: string) => {
+ setActiveFilters((prev: Record) => ({
+ ...prev,
+ [filterKey]: value,
+ }))
+ }
+
+ // Find the maximum value for scaling the bars
+ const maxValue = chartData.length > 0 ? Math.max(...chartData.map((item) => item.value)) : 0
+
+ // State for expand/collapse button
+ const [isExpanded, setIsExpanded] = useState(false)
+
+ // Fixed height for all states (loading, error, loaded collapsed)
+ const FIXED_HEIGHT = 300 // px
+ const BUTTON_AREA_HEIGHT = 40 // px
+ const CHART_HEIGHT = FIXED_HEIGHT - BUTTON_AREA_HEIGHT // px
+
+ const skeletonData = [
+ { label: 'Loading', value: 0, rawValue: 0 },
+ { label: 'Loading', value: 0, rawValue: 0 },
+ { label: 'Loading', value: 0, rawValue: 0 },
+ ]
+
+ const displayData = dataLoading ? skeletonData : chartData || []
+
+ return (
+
+
+
+
+ {/* Filters and toggle */}
+
+ {filters && activeFilters && setFilterValue && (
+
+ {Object.entries(filters).map(([filterKey, filterConfig]) => (
+
+ ))}
+
+ )}
+
+
+
+
+
+ {dataError ? (
+
+ ) : view === 'chart' ? (
+
+ {chartData.length > 0 ? (
+
+ {displayData.map((item, index) => (
+
+ {/* Text above the bar */}
+
+ {item.label}
+ {item.value < 1 ? '<1%' : `${item.value}%`}
+
+
+ {/* Progress bar */}
+
+ {/* Background pattern for the entire bar */}
+
+
+ {/* Filled portion of the bar */}
+
+ {/* Foreground pattern for the filled portion */}
+
+
+
+
+ ))}
+
+ ) : (
+
+
+ No responses match those filters. Maybe next year?
+
+
+ setActiveFilters(
+ filterColumns.reduce(
+ (acc: Record, col: string) => ({
+ ...acc,
+ [col]: NO_FILTER,
+ }),
+ {}
+ )
+ )
+ }
+ >
+ Clear filters
+
+
+ )}
+
+ ) : (
+
+ {generateSQLQuery ? (
+ {generateSQLQuery(activeFilters)}
+ ) : (
+
+ {`// Function call: ${functionName}(${JSON.stringify(buildFunctionParams(activeFilters), null, 2)})`}
+
+ )}
+
+ )}
+
+ {/* Expand button overlay - only show for chart view */}
+ {view === 'chart' &&
+ !isExpanded &&
+ !dataLoading &&
+ !dataError &&
+ chartData.length > 3 && (
+
+ setIsExpanded(true)}
+ className="shadow-sm"
+ >
+ Show more
+
+
+ )}
+
+
+
+ )
+}
+
+// Helper to build SQL WHERE clauses from active filters
+// Accepts optional initialClauses for charts that need extra constraints
+// (e.g., "column IS NOT NULL")
+// Note: This function is used by chart components that generate SQL queries
+export function buildWhereClause(
+ activeFilters: Record,
+ initialClauses: string[] = []
+) {
+ const whereClauses: string[] = [...initialClauses]
+
+ for (const [column, value] of Object.entries(activeFilters)) {
+ if (value && value !== NO_FILTER) {
+ whereClauses.push(`${column} = '${value}'`)
+ }
+ }
+
+ return whereClauses.length > 0 ? `WHERE ${whereClauses.join('\n AND ')}` : ''
+}
+
+// Dropdown filter component
+function SurveyFilter({
+ filterKey,
+ filterConfig,
+ selectedValue,
+ setFilterValue,
+}: {
+ filterKey: string
+ filterConfig: {
+ label: string
+ options: { value: string; label: string }[]
+ }
+ selectedValue: string
+ setFilterValue: (filterKey: string, value: string) => void
+}) {
+ return (
+
+
+ }
+ >
+
+
{filterConfig.label}
+ {selectedValue !== NO_FILTER &&
{selectedValue}
}
+
+
+
+
+ {filterConfig.options
+ .filter((opt) => opt.value !== NO_FILTER)
+ .map((option) => (
+ setFilterValue(filterKey, option.value)}
+ className={selectedValue === option.value ? 'text-brand-600' : ''}
+ >
+ {option.label}
+
+ ))}
+
+ {selectedValue !== NO_FILTER && (
+
+ setFilterValue(filterKey, NO_FILTER)}
+ className="text-foreground-lighter"
+ >
+ Clear
+
+
+ )}
+
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/SurveyPullQuote.tsx b/apps/www/components/SurveyResults/SurveyPullQuote.tsx
new file mode 100644
index 0000000000000..de3bea6c1b62b
--- /dev/null
+++ b/apps/www/components/SurveyResults/SurveyPullQuote.tsx
@@ -0,0 +1,48 @@
+import Image from 'next/image'
+import { SurveySectionBreak } from './SurveySectionBreak'
+
+export function SurveyPullQuote({
+ quote,
+ author,
+ authorPosition,
+ authorAvatar,
+}: {
+ quote: string
+ author: string
+ authorPosition: string
+ authorAvatar: string
+}) {
+ return (
+ <>
+
+
+ >
+ )
+}
diff --git a/apps/www/components/SurveyResults/SurveyRankedAnswersPair.tsx b/apps/www/components/SurveyResults/SurveyRankedAnswersPair.tsx
new file mode 100644
index 0000000000000..70ab8054ebe4a
--- /dev/null
+++ b/apps/www/components/SurveyResults/SurveyRankedAnswersPair.tsx
@@ -0,0 +1,56 @@
+export function SurveyRankedAnswersPair({
+ rankedAnswersPair,
+}: {
+ rankedAnswersPair: Array<{ label: string; answers: string[] }>
+}) {
+ return (
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/SurveySectionBreak.tsx b/apps/www/components/SurveyResults/SurveySectionBreak.tsx
new file mode 100644
index 0000000000000..12612a9f0d8b0
--- /dev/null
+++ b/apps/www/components/SurveyResults/SurveySectionBreak.tsx
@@ -0,0 +1,19 @@
+import { cn } from 'ui'
+
+export function SurveySectionBreak({ className }: { className?: string }) {
+ return (
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/SurveyStatCard.tsx b/apps/www/components/SurveyResults/SurveyStatCard.tsx
new file mode 100644
index 0000000000000..b4b2a7c44d3ac
--- /dev/null
+++ b/apps/www/components/SurveyResults/SurveyStatCard.tsx
@@ -0,0 +1,117 @@
+import { useEffect, useRef, useState } from 'react'
+
+const ANIMATION_DURATION = 600
+
+// These values are calculated via static SQL queries under 'Key Stats' and rounded for display in data/surveys/state-of-startups-2025.tsx file
+export function SurveyStatCard({ label, percent }: { label: string; percent: number }) {
+ const [displayValue, setDisplayValue] = useState(0)
+ const [hasAnimated, setHasAnimated] = useState(false)
+ const [shouldAnimateBar, setShouldAnimateBar] = useState(false)
+ const cardRef = useRef(null)
+
+ useEffect(() => {
+ if (!percent || hasAnimated) return
+
+ const observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting && !hasAnimated) {
+ setHasAnimated(true)
+ setShouldAnimateBar(true)
+
+ // Start counting animation
+ const startTime = Date.now()
+ const duration = ANIMATION_DURATION
+ const startValue = 0
+ const endValue = percent
+
+ const animate = () => {
+ const elapsed = Date.now() - startTime
+ const progress = Math.min(elapsed / duration, 1)
+
+ // Easing function for smooth animation
+ const easeOutQuart = 1 - Math.pow(1 - progress, 4)
+ const currentValue = Math.round(startValue + (endValue - startValue) * easeOutQuart)
+
+ setDisplayValue(currentValue)
+
+ if (progress < 1) {
+ requestAnimationFrame(animate)
+ }
+ }
+
+ requestAnimationFrame(animate)
+ }
+ })
+ },
+ { threshold: 0.1 }
+ )
+
+ const currentCardRef = cardRef.current
+ if (currentCardRef) {
+ observer.observe(currentCardRef)
+ }
+
+ return () => {
+ if (currentCardRef) {
+ observer.unobserve(currentCardRef)
+ }
+ }
+ }, [percent, hasAnimated])
+
+ return (
+
+ {/* Progress bar */}
+
+ {/* Background pattern for the entire bar */}
+
+
+ {/* Filled portion of the bar */}
+
+ {/* Foreground pattern for the filled portion */}
+
+
+
+ {/* Text */}
+
+
+ {displayValue}
+ %
+
+
{label}
+
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/SurveySummarizedAnswer.tsx b/apps/www/components/SurveyResults/SurveySummarizedAnswer.tsx
new file mode 100644
index 0000000000000..6ae7fcde0952c
--- /dev/null
+++ b/apps/www/components/SurveyResults/SurveySummarizedAnswer.tsx
@@ -0,0 +1,104 @@
+import { useEffect, useState } from 'react'
+import './surveyResults.css'
+
+const ROTATION_DURATION = 4000
+
+export function SurveySummarizedAnswer({ label, answers }: { label: string; answers: string[] }) {
+ const [currentIndex, setCurrentIndex] = useState(0)
+ const [isAnimating, setIsAnimating] = useState(false)
+ const [isBlinking, setIsBlinking] = useState(false)
+
+ useEffect(() => {
+ if (answers.length <= 1) return
+
+ const interval = setInterval(() => {
+ setCurrentIndex((prev) => (prev + 1) % answers.length)
+ }, ROTATION_DURATION)
+
+ return () => clearInterval(interval)
+ }, [answers.length])
+
+ useEffect(() => {
+ // First, reset the bar to 0%
+ setIsAnimating(false)
+ setIsBlinking(false)
+
+ // Then start the animation after a brief delay to ensure the reset is applied
+ const startTimer = setTimeout(() => {
+ setIsAnimating(true)
+ }, 50)
+
+ // Stop the animation after 3 seconds
+ const stopTimer = setTimeout(() => {
+ setIsAnimating(false)
+ }, 4050)
+
+ // Start blinking shortly before text change
+ const blinkTimer = setTimeout(() => {
+ setIsBlinking(true)
+ }, 3700)
+
+ // Stop blinking when text changes
+ const stopBlinkTimer = setTimeout(() => {
+ setIsBlinking(false)
+ }, ROTATION_DURATION)
+
+ return () => {
+ clearTimeout(startTimer)
+ clearTimeout(stopTimer)
+ clearTimeout(blinkTimer)
+ clearTimeout(stopBlinkTimer)
+ }
+ }, [currentIndex])
+
+ return (
+
+
+ {answers[currentIndex]}
+
+ {/* Decorative progress bar */}
+
+ {/* Background pattern for the entire bar */}
+
+
+ {/* Filled portion of the bar - timer-based */}
+
+ {/* Foreground pattern for the filled portion */}
+
+
+
+
+ {label}
+
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/SurveyWordCloud.tsx b/apps/www/components/SurveyResults/SurveyWordCloud.tsx
new file mode 100644
index 0000000000000..04c9be10e9387
--- /dev/null
+++ b/apps/www/components/SurveyResults/SurveyWordCloud.tsx
@@ -0,0 +1,188 @@
+import { useEffect, useState } from 'react'
+
+const TIMER_DURATION = 3000
+
+export function SurveyWordCloud({
+ answers,
+ label,
+}: {
+ answers: { text: string; count: number }[]
+ label: string
+}) {
+ const [currentItems, setCurrentItems] = useState<{ text: string; count: number }[]>([])
+ const [isRotating, setIsRotating] = useState(false)
+ const [scramblingTexts, setScramblingTexts] = useState([])
+
+ // Calculate the range within the current context
+ const counts = answers.map((answer) => answer.count)
+ const maxCount = Math.max(...counts)
+
+ // Initialize with first 12 items
+ useEffect(() => {
+ const initialItems = answers.slice(0, 12)
+ setCurrentItems(initialItems)
+ setScramblingTexts(initialItems.map((item) => item.text))
+ }, [answers])
+
+ // Start rotation after 3 seconds
+ useEffect(() => {
+ if (answers.length <= 12) return // No need to rotate if we have 12 or fewer items
+
+ const timer = setTimeout(() => {
+ setIsRotating(true)
+ }, TIMER_DURATION)
+
+ return () => clearTimeout(timer)
+ }, [answers.length])
+
+ // Airport board scramble effect
+ useEffect(() => {
+ if (!isRotating || answers.length <= 12 || scramblingTexts.length === 0) return
+
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ const characterDelay = 50 // 50ms between each character change
+
+ const rotateItems = () => {
+ const newItems = currentItems.map((item, index) => {
+ // Calculate next item index with wrapping
+ const nextItemIndex = (index + Math.floor(Date.now() / TIMER_DURATION)) % answers.length
+ return answers[nextItemIndex]
+ })
+
+ setCurrentItems(newItems)
+
+ // Start scramble animation for each item
+ newItems.forEach((newItem, index) => {
+ const oldText = scramblingTexts[index] || ''
+ const newText = newItem.text
+ const maxLength = Math.max(oldText.length, newText.length)
+
+ for (let charIndex = 0; charIndex < maxLength; charIndex++) {
+ setTimeout(() => {
+ const newChar = newText[charIndex]
+
+ // If we're past the old text length, just show the new char
+ if (charIndex >= oldText.length) {
+ if (newChar) {
+ // Only animate if there's actually a character to show
+ setScramblingTexts((prev) => {
+ const updated = [...prev]
+ updated[index] = (updated[index] || '').substring(0, charIndex) + newChar
+ return updated
+ })
+ }
+ return
+ }
+
+ // If we're past the new text length, remove the character
+ if (charIndex >= newText.length) {
+ setScramblingTexts((prev) => {
+ const updated = [...prev]
+ updated[index] = (updated[index] || '').substring(0, charIndex)
+ return updated
+ })
+ return
+ }
+
+ // Scramble through alphabet for existing characters that are being replaced
+ let scrambleCount = 0
+ const scrambleInterval = setInterval(() => {
+ const randomChar = alphabet[Math.floor(Math.random() * alphabet.length)]
+
+ setScramblingTexts((prev) => {
+ const updated = [...prev]
+ const currentText = updated[index] || ''
+ updated[index] =
+ currentText.substring(0, charIndex) +
+ randomChar +
+ currentText.substring(charIndex + 1)
+ return updated
+ })
+
+ scrambleCount++
+ if (scrambleCount >= 8) {
+ // Scramble 8 times before settling
+ clearInterval(scrambleInterval)
+ setScramblingTexts((prev) => {
+ const updated = [...prev]
+ const currentText = updated[index] || ''
+ updated[index] =
+ currentText.substring(0, charIndex) +
+ newChar +
+ currentText.substring(charIndex + 1)
+ return updated
+ })
+ }
+ }, characterDelay)
+ }, charIndex * 100) // Stagger each character
+ }
+ })
+ }
+
+ // Rotate every 3 seconds
+ const interval = setInterval(rotateItems, TIMER_DURATION)
+
+ return () => clearInterval(interval)
+ }, [isRotating, answers, scramblingTexts, currentItems])
+
+ return (
+
+
+ {currentItems.map(({ text, count }, index) => (
+
+ {/* Progress bar */}
+ {count && (
+
+ {/* Background pattern for the entire bar */}
+
+
+ {/* Filled portion of the bar */}
+
+ {/* Foreground pattern for the filled portion */}
+
+
+
+ )}
+
+ {scramblingTexts[index] || text}
+
+
+ ))}
+
+
+ {label}
+
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/charts/AICodingToolsChart.tsx b/apps/www/components/SurveyResults/charts/AICodingToolsChart.tsx
new file mode 100644
index 0000000000000..66e8acc83f8e8
--- /dev/null
+++ b/apps/www/components/SurveyResults/charts/AICodingToolsChart.tsx
@@ -0,0 +1,49 @@
+import { SurveyChart, buildWhereClause } from '../SurveyChart'
+
+function generateAICodingToolsSQL(activeFilters: Record) {
+ const whereClause = buildWhereClause(activeFilters)
+
+ return `WITH ai_coding_tools_mapping AS (
+ SELECT
+ id,
+ CASE
+ WHEN technology IN (
+ 'Cursor',
+ 'Windsurf',
+ 'Cline',
+ 'Visual Studio Code',
+ 'Lovable',
+ 'Bolt',
+ 'v0',
+ 'Tempo',
+ 'None'
+ ) THEN technology
+ WHEN LOWER(technology) LIKE '%claude%' THEN 'Claude or Claude Code'
+ WHEN LOWER(technology) = 'chatgpt' THEN 'ChatGPT'
+ ELSE 'Other'
+ END AS technology_clean
+ FROM (
+ SELECT id, unnest(ai_coding_tools) AS technology
+ FROM responses_2025
+ ${whereClause}
+ ) sub
+ )
+ SELECT
+ technology_clean AS technology,
+ COUNT(DISTINCT id) AS total
+ FROM ai_coding_tools_mapping
+ GROUP BY technology_clean
+ ORDER BY total DESC;`
+}
+
+export function AICodingToolsChart() {
+ return (
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/charts/AIModelsChart.tsx b/apps/www/components/SurveyResults/charts/AIModelsChart.tsx
new file mode 100644
index 0000000000000..a7dbee1367c18
--- /dev/null
+++ b/apps/www/components/SurveyResults/charts/AIModelsChart.tsx
@@ -0,0 +1,48 @@
+import { SurveyChart, buildWhereClause } from '../SurveyChart'
+
+function generateAIModelsSQL(activeFilters: Record) {
+ const whereClause = buildWhereClause(activeFilters)
+
+ return `WITH ai_models_used_mapping AS (
+ SELECT
+ id,
+ CASE
+ WHEN technology IN (
+ 'OpenAI',
+ 'Anthropic/Claude',
+ 'Hugging Face',
+ 'Custom models',
+ 'SageMaker',
+ 'Bedrock',
+ 'Cohere',
+ 'Mistral'
+ ) THEN technology
+ WHEN LOWER(technology) LIKE '%gemini%' THEN 'Gemini'
+ WHEN LOWER(technology) = 'deepseek' THEN 'DeepSeek'
+ ELSE 'Other'
+ END AS technology_clean
+ FROM (
+ SELECT id, unnest(ai_models_used) AS technology
+ FROM responses_2025
+ ${whereClause}
+ ) sub
+ )
+ SELECT
+ technology_clean AS technology,
+ COUNT(DISTINCT id) AS total
+ FROM ai_models_used_mapping
+ GROUP BY technology_clean
+ ORDER BY total DESC;`
+}
+
+export function AIModelsChart() {
+ return (
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/charts/AcceleratorParticipationChart.tsx b/apps/www/components/SurveyResults/charts/AcceleratorParticipationChart.tsx
new file mode 100644
index 0000000000000..6ee816aaf80a3
--- /dev/null
+++ b/apps/www/components/SurveyResults/charts/AcceleratorParticipationChart.tsx
@@ -0,0 +1,36 @@
+import { SurveyChart, buildWhereClause } from '../SurveyChart'
+
+function generateAcceleratorParticipationSQL(activeFilters: Record) {
+ const whereClause = buildWhereClause(activeFilters, ['accelerator_participation IS NOT NULL'])
+
+ return `WITH accelerator_mapping AS (
+ SELECT
+ accelerator_participation,
+ CASE
+ WHEN accelerator_participation IN ('YC', 'Techstars', 'EF', '500 Global', 'Plug and Play', 'Antler') THEN accelerator_participation
+ WHEN accelerator_participation = 'Did not participate in an accelerator' THEN NULL
+ ELSE 'Other'
+ END AS accelerator_clean
+ FROM responses_2025
+ ${whereClause ? '\n ' + whereClause : ''}
+)
+SELECT
+ accelerator_clean AS accelerator_participation,
+ COUNT(*) AS total
+FROM accelerator_mapping
+WHERE accelerator_clean IS NOT NULL
+GROUP BY accelerator_clean
+ORDER BY total DESC;`
+}
+
+export function AcceleratorParticipationChart() {
+ return (
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/charts/BiggestChallengeChart.tsx b/apps/www/components/SurveyResults/charts/BiggestChallengeChart.tsx
new file mode 100644
index 0000000000000..4a549ac61dbd1
--- /dev/null
+++ b/apps/www/components/SurveyResults/charts/BiggestChallengeChart.tsx
@@ -0,0 +1,34 @@
+import { SurveyChart, buildWhereClause } from '../SurveyChart'
+
+function generateBiggestChallengeSQL(activeFilters: Record) {
+ const whereClause = buildWhereClause(activeFilters, ['biggest_challenge IS NOT NULL'])
+
+ return `WITH biggest_challenge_mapping AS (
+ SELECT
+ biggest_challenge,
+ CASE
+ WHEN biggest_challenge IN ('Customer acquisition', 'Technical complexity', 'Product-market fit', 'Product-market fit', 'Fundraising', 'Hiring', 'Other') THEN biggest_challenge
+ ELSE 'Other'
+ END AS biggest_challenge_clean
+ FROM responses_2025
+ ${whereClause}
+)
+SELECT
+ biggest_challenge_clean AS biggest_challenge,
+ COUNT(*) AS total
+FROM biggest_challenge_mapping
+GROUP BY biggest_challenge_clean
+ORDER BY total DESC;`
+}
+
+export function BiggestChallengeChart() {
+ return (
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/charts/DatabasesChart.tsx b/apps/www/components/SurveyResults/charts/DatabasesChart.tsx
new file mode 100644
index 0000000000000..e78e0f11f962d
--- /dev/null
+++ b/apps/www/components/SurveyResults/charts/DatabasesChart.tsx
@@ -0,0 +1,45 @@
+import { SurveyChart, buildWhereClause } from '../SurveyChart'
+
+function generateDatabasesSQL(activeFilters: Record) {
+ const whereClause = buildWhereClause(activeFilters)
+
+ return `WITH database_mapping AS (
+ SELECT
+ id,
+ CASE
+ WHEN technology IN (
+ 'Supabase',
+ 'PostgreSQL',
+ 'MySQL',
+ 'MongoDB',
+ 'Redis',
+ 'Firebase',
+ 'SQLite'
+ ) THEN technology
+ ELSE 'Other'
+ END AS technology_clean
+ FROM (
+ SELECT id, unnest(databases) AS technology
+ FROM responses_2025
+ ${whereClause}
+ ) sub
+ )
+ SELECT
+ technology_clean AS technology,
+ COUNT(DISTINCT id) AS total
+ FROM database_mapping
+ GROUP BY technology_clean
+ ORDER BY total DESC;`
+}
+
+export function DatabasesChart() {
+ return (
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/charts/FundingStageChart.tsx b/apps/www/components/SurveyResults/charts/FundingStageChart.tsx
new file mode 100644
index 0000000000000..fef480e165fd2
--- /dev/null
+++ b/apps/www/components/SurveyResults/charts/FundingStageChart.tsx
@@ -0,0 +1,33 @@
+import { SurveyChart, buildWhereClause } from '../SurveyChart'
+
+function generateFundingStageSQL(activeFilters: Record) {
+ const whereClause = buildWhereClause(activeFilters)
+
+ return `SELECT
+ funding_stage,
+ COUNT(*) AS total
+ FROM responses_2025${whereClause ? '\n' + whereClause : ''}
+ GROUP BY funding_stage
+ ORDER BY CASE
+ WHEN funding_stage = 'Bootstrapped' THEN 1
+ WHEN funding_stage = 'Pre-seed' THEN 2
+ WHEN funding_stage = 'Seed' THEN 3
+ WHEN funding_stage = 'Series A' THEN 4
+ WHEN funding_stage = 'Series B' THEN 5
+ WHEN funding_stage = 'Series C' THEN 6
+ WHEN funding_stage = 'Series D or later' THEN 7
+ ELSE 8
+ END;`
+}
+
+export function FundingStageChart() {
+ return (
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/charts/IndustryChart.tsx b/apps/www/components/SurveyResults/charts/IndustryChart.tsx
new file mode 100644
index 0000000000000..15d4d46df220a
--- /dev/null
+++ b/apps/www/components/SurveyResults/charts/IndustryChart.tsx
@@ -0,0 +1,36 @@
+import { SurveyChart, buildWhereClause } from '../SurveyChart'
+
+function generateIndustrySQL(activeFilters: Record) {
+ const whereClause = buildWhereClause(activeFilters, ['industry IS NOT NULL'])
+
+ return `WITH industry_mapping AS (
+ SELECT
+ industry,
+ CASE
+ WHEN industry = 'Developer tools and platforms' THEN 'Dev tools'
+ WHEN industry = 'AI / ML tools' THEN 'AI / ML'
+ WHEN industry IN ('SaaS', 'Dev tools', 'AI / ML', 'Consumer', 'Education', 'eCommerce', 'Fintech', 'Healthtech') THEN industry
+ ELSE 'Other'
+ END AS industry_clean
+ FROM responses_2025
+ ${whereClause}
+)
+SELECT
+ industry_clean AS industry,
+ COUNT(*) AS total
+FROM industry_mapping
+GROUP BY industry_clean
+ORDER BY total DESC;`
+}
+
+export function IndustryChart() {
+ return (
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/charts/InitialPayingCustomersChart.tsx b/apps/www/components/SurveyResults/charts/InitialPayingCustomersChart.tsx
new file mode 100644
index 0000000000000..35cdeb5a36521
--- /dev/null
+++ b/apps/www/components/SurveyResults/charts/InitialPayingCustomersChart.tsx
@@ -0,0 +1,46 @@
+import { SurveyChart, buildWhereClause } from '../SurveyChart'
+
+function generateInitialPayingCustomersSQL(activeFilters: Record) {
+ const whereClause = buildWhereClause(activeFilters)
+
+ return `WITH customer_source_mapping AS (
+ SELECT
+ id,
+ CASE
+ WHEN source IN (
+ 'Personal/professional network',
+ 'Inbound from social media (Twitter, LinkedIn, etc.)',
+ 'Cold outreach or sales',
+ 'Content (blog, newsletter, SEO)',
+ 'Developer communities (Discord, Slack, Reddit, etc.)',
+ 'Open source users who converted',
+ 'Accelerators/incubators',
+ 'Hacker News or Product Hunt'
+ ) THEN source
+ ELSE 'Other'
+ END AS source_clean
+ FROM (
+ SELECT id, unnest(initial_paying_customers) AS source
+ FROM responses_2025
+ ${whereClause}
+ ) sub
+ )
+ SELECT
+ source_clean AS source,
+ COUNT(DISTINCT id) AS respondents
+ FROM customer_source_mapping
+ GROUP BY source_clean
+ ORDER BY respondents DESC;`
+}
+
+export function InitialPayingCustomersChart() {
+ return (
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/charts/LocationChart.tsx b/apps/www/components/SurveyResults/charts/LocationChart.tsx
new file mode 100644
index 0000000000000..2bec94cf5dbf4
--- /dev/null
+++ b/apps/www/components/SurveyResults/charts/LocationChart.tsx
@@ -0,0 +1,24 @@
+import { SurveyChart, buildWhereClause } from '../SurveyChart'
+
+function generateLocationSQL(activeFilters: Record) {
+ const whereClause = buildWhereClause(activeFilters)
+
+ return `SELECT
+ location,
+ COUNT(*) AS total
+FROM responses_2025${whereClause ? '\n' + whereClause : ''}
+GROUP BY location
+ORDER BY total DESC;`
+}
+
+export function LocationChart() {
+ return (
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/charts/NewIdeasChart.tsx b/apps/www/components/SurveyResults/charts/NewIdeasChart.tsx
new file mode 100644
index 0000000000000..6be14f42f1ef1
--- /dev/null
+++ b/apps/www/components/SurveyResults/charts/NewIdeasChart.tsx
@@ -0,0 +1,48 @@
+import { SurveyChart, buildWhereClause } from '../SurveyChart'
+
+function generateNewIdeasSQL(activeFilters: Record) {
+ const whereClause = buildWhereClause(activeFilters)
+
+ return `WITH new_ideas_mapping AS (
+ SELECT
+ id,
+ CASE
+ WHEN avenue IN (
+ 'Hacker News',
+ 'GitHub',
+ 'Product Hunt',
+ 'Twitter/X',
+ 'Reddit',
+ 'YouTube',
+ 'Podcasts',
+ 'Blogs / Newsletters',
+ 'Discord / Slack communities',
+ 'Conferences / Meetups'
+ ) THEN avenue
+ ELSE 'Other'
+ END AS avenue_clean
+ FROM (
+ SELECT id, unnest(new_ideas) AS avenue
+ FROM responses_2025
+ ${whereClause}
+ ) sub
+ )
+ SELECT
+ avenue_clean AS avenue,
+ COUNT(DISTINCT id) AS total
+ FROM new_ideas_mapping
+ GROUP BY avenue_clean
+ ORDER BY total DESC;`
+}
+
+export function NewIdeasChart() {
+ return (
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/charts/RegularSocialMediaUseChart.tsx b/apps/www/components/SurveyResults/charts/RegularSocialMediaUseChart.tsx
new file mode 100644
index 0000000000000..fd4395bd629c8
--- /dev/null
+++ b/apps/www/components/SurveyResults/charts/RegularSocialMediaUseChart.tsx
@@ -0,0 +1,49 @@
+import { SurveyChart, buildWhereClause } from '../SurveyChart'
+
+function generateRegularSocialMediaUseSQL(activeFilters: Record) {
+ const whereClause = buildWhereClause(activeFilters)
+
+ return `WITH regular_social_media_use_mapping AS (
+ SELECT
+ id,
+ CASE
+ WHEN platform IN (
+ 'X (Twitter)',
+ 'Threads',
+ 'BlueSky',
+ 'LinkedIn',
+ 'Reddit',
+ 'TikTok',
+ 'Instagram',
+ 'YouTube',
+ 'Mastodon',
+ 'Discord',
+ 'I’ve given up social media'
+ ) THEN platform
+ ELSE 'Other'
+ END AS platform_clean
+ FROM (
+ SELECT id, unnest(regular_social_media_use) AS platform
+ FROM responses_2025
+ ${whereClause}
+ ) sub
+ )
+ SELECT
+ platform_clean AS platform,
+ COUNT(DISTINCT id) AS total
+ FROM regular_social_media_use_mapping
+ GROUP BY platform_clean
+ ORDER BY total DESC;`
+}
+
+export function RegularSocialMediaUseChart() {
+ return (
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/charts/RoleChart.tsx b/apps/www/components/SurveyResults/charts/RoleChart.tsx
new file mode 100644
index 0000000000000..6ed52273489b4
--- /dev/null
+++ b/apps/www/components/SurveyResults/charts/RoleChart.tsx
@@ -0,0 +1,32 @@
+import { SurveyChart, buildWhereClause } from '../SurveyChart'
+
+function generateRoleSQL(activeFilters: Record) {
+ const whereClause = buildWhereClause(activeFilters)
+
+ return `SELECT
+ CASE
+ WHEN role = 'Founder / Co-founder' THEN 'Founder'
+ WHEN role IN ('Engineer', 'Founder / Co-founder') THEN role
+ ELSE 'Other'
+ END AS role,
+ COUNT(*) AS total
+FROM responses_2025${whereClause ? '\n' + whereClause : ''}
+GROUP BY CASE
+ WHEN role = 'Founder / Co-founder' THEN 'Founder'
+ WHEN role IN ('Engineer', 'Founder / Co-founder') THEN role
+ ELSE 'Other'
+ END
+ORDER BY total DESC;`
+}
+
+export function RoleChart() {
+ return (
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/charts/SalesToolsChart.tsx b/apps/www/components/SurveyResults/charts/SalesToolsChart.tsx
new file mode 100644
index 0000000000000..c65d9ba91d207
--- /dev/null
+++ b/apps/www/components/SurveyResults/charts/SalesToolsChart.tsx
@@ -0,0 +1,45 @@
+import { SurveyChart, buildWhereClause } from '../SurveyChart'
+
+function generateSalesToolsSQL(activeFilters: Record) {
+ const whereClause = buildWhereClause(activeFilters)
+
+ return `WITH customer_tool_mapping AS (
+ SELECT
+ id,
+ CASE
+ WHEN tool IN (
+ 'HubSpot',
+ 'Salesforce',
+ 'Pipedrive',
+ 'Close.com',
+ 'Notion / Airtable',
+ 'Google Sheets',
+ 'We don’t have a formal CRM or sales tool yet'
+ ) THEN tool
+ ELSE 'Other'
+ END AS tool_clean
+ FROM (
+ SELECT id, unnest(sales_tools) AS tool
+ FROM responses_2025
+ ${whereClause}
+ ) sub
+ )
+ SELECT
+ tool_clean AS tool,
+ COUNT(DISTINCT id) AS respondents
+ FROM customer_tool_mapping
+ GROUP BY tool_clean
+ ORDER BY respondents DESC;`
+}
+
+export function SalesToolsChart() {
+ return (
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/charts/WorldOutlookChart.tsx b/apps/www/components/SurveyResults/charts/WorldOutlookChart.tsx
new file mode 100644
index 0000000000000..e820e3556031e
--- /dev/null
+++ b/apps/www/components/SurveyResults/charts/WorldOutlookChart.tsx
@@ -0,0 +1,24 @@
+import { SurveyChart, buildWhereClause } from '../SurveyChart'
+
+function generateWorldOutlookSQL(activeFilters: Record) {
+ const whereClause = buildWhereClause(activeFilters)
+
+ return `SELECT
+ world_outlook,
+ COUNT(*) AS total
+FROM responses_2025${whereClause ? '\n' + whereClause : ''}
+GROUP BY world_outlook
+ORDER BY total DESC;`
+}
+
+export function WorldOutlookChart() {
+ return (
+
+ )
+}
diff --git a/apps/www/components/SurveyResults/surveyResults.css b/apps/www/components/SurveyResults/surveyResults.css
new file mode 100644
index 0000000000000..213c9638b102c
--- /dev/null
+++ b/apps/www/components/SurveyResults/surveyResults.css
@@ -0,0 +1,34 @@
+@keyframes terminalLine {
+ 0% {
+ clip-path: inset(0 100% 0 0);
+ }
+ 20% {
+ clip-path: inset(0 80% 0 0);
+ }
+ 50% {
+ clip-path: inset(0 40% 0 0);
+ }
+ 70% {
+ clip-path: inset(0 20% 0 0);
+ }
+ 80% {
+ clip-path: inset(0 0% 0 0);
+ }
+ 90% {
+ clip-path: inset(0 0 0 50%);
+ }
+ 100% {
+ clip-path: inset(0 0 0 100%);
+ }
+}
+
+@keyframes blink {
+ 0%,
+ 49% {
+ opacity: 1;
+ }
+ 50%,
+ 100% {
+ opacity: 0;
+ }
+}
diff --git a/apps/www/data/home/content.tsx b/apps/www/data/home/content.tsx
index 6d04e75a4d5c8..6cdaa881e0ee5 100644
--- a/apps/www/data/home/content.tsx
+++ b/apps/www/data/home/content.tsx
@@ -5,7 +5,7 @@ import VideoWithHighlights from 'components/VideoWithHighlights'
import ProductModules from '../ProductModules'
import { useSendTelemetryEvent } from 'lib/telemetry'
-import Tweets from 'data/tweets/Tweets.json'
+import tweets from 'shared-data/tweets'
import MainProducts from 'data/MainProducts'
export default () => {
@@ -204,7 +204,7 @@ export default () => {
>
),
- tweets: Tweets.slice(0, 18),
+ tweets: tweets.slice(0, 18),
},
}
}
diff --git a/apps/www/data/surveys/state-of-startups-2025.tsx b/apps/www/data/surveys/state-of-startups-2025.tsx
index 161b729dd8b74..238a76209b859 100644
--- a/apps/www/data/surveys/state-of-startups-2025.tsx
+++ b/apps/www/data/surveys/state-of-startups-2025.tsx
@@ -1,18 +1,687 @@
-export default (isMobile?: boolean) => ({
- metaTitle: 'State of Startups 2025',
+const stateOfStartupsData = {
+ metaTitle: 'State of Startups 2025 | Supabase',
metaDescription:
- 'Take the survey and learn the latest trends among builders in tech stacks, AI usage, problem domains, and more.',
+ 'The latest trends among builders in tech stacks, AI usage, problem domains, and more.',
metaImage: '/images/state-of-startups/2025/state-of-startups-og.png',
- docsUrl: '',
heroSection: {
title: 'State of Startups 2025',
- subheader: (
- <>
- There's never been a better time to build.
-
- Take our State of Startups survey and tell us how you're building.
- >
- ),
- className: '[&_h1]:max-w-2xl',
+ subheader:
+ 'We surveyed over 2,000 startup founders and builders to uncover what’s powering modern startups: their stacks, their go-to-market motion, and their approach to AI.',
+ cta: 'This report is built for builders.',
},
-})
+ pageChapters: [
+ {
+ title: 'Who’s Building Startups',
+ shortTitle: 'Founder and Company',
+ description:
+ 'Today’s startup ecosystem is dominated by young, technical builders shipping fast with lean teams.',
+ pullQuote: {
+ quote:
+ 'Our team is just two people at the moment. We’re funding the proof-of-concept stage out of our own pockets.',
+ author: 'Richard Kranendonk',
+ authorPosition: 'CEO, Thinking Security Works',
+ authorAvatar: '/images/state-of-startups/quote-avatars/richard-k-120x120.jpg',
+ },
+ sections: [
+ {
+ title: 'Roles and Experience',
+ description:
+ 'Founders are overwhelmingly technical and under 40, with most building their first company.',
+ stats: [
+ { percent: 81, label: 'Founders that are technical' },
+ { percent: 82, label: 'Founders that are under 40' },
+ { percent: 36, label: 'Founders that are repeat founders' },
+ ],
+ charts: ['RoleChart'],
+ wordCloud: undefined,
+ summarizedAnswer: undefined,
+ rankedAnswersPair: undefined,
+ },
+ {
+ title: 'Team Size and Funding',
+ description:
+ 'Startups are mostly bootstrapped or at early stages of funding. They are small teams, and usually less than a year old.',
+ stats: [
+ { percent: 91, label: 'Startups with 10 or fewer employees' },
+ { percent: 66, label: 'Startups under one year old' },
+ { percent: 6, label: 'Startups over 5 years old' },
+ ],
+ charts: ['FundingStageChart'],
+ wordCloud: undefined,
+ summarizedAnswer: undefined,
+ rankedAnswersPair: undefined,
+ },
+ {
+ title: 'Where They’re Based',
+ description:
+ 'Startups are building globally, but North America—especially San Francisco—remains overrepresented. Europe and Asia also feature prominently, with hubs like Toronto and NYC following close behind.',
+ stats: [
+ { percent: 25, label: 'Global startups based in Europe' },
+ { percent: 19, label: 'North American startups based in San Francisco' },
+ { percent: 9, label: 'North American startups based in New York City' },
+ ],
+ charts: ['LocationChart'],
+ wordCloud: undefined,
+ summarizedAnswer: undefined,
+ rankedAnswersPair: undefined,
+ },
+ ],
+ },
+ {
+ title: 'What Startups are Building',
+ shortTitle: 'Product and Market',
+ description:
+ 'Startups are still experimenting. They’re building a diverse mix of software products, iterating quickly, and pursuing monetization selectively.',
+ pullQuote: {
+ quote:
+ 'We’re building an end-to-end system for wedding planners, all running as one SvelteKit / Supabase instance.',
+ author: 'Waldemar Pross',
+ authorPosition: 'CTO, Peach Perfect Weddings',
+ authorAvatar: '/images/state-of-startups/quote-avatars/waldemar-k-120x120.jpg',
+ },
+ sections: [
+ {
+ title: 'Industries and Focus',
+ description:
+ 'Under-30s gravitate toward AI-driven productivity, education, and social tools; areas where rapid iteration and novelty matter. Over-50s skew toward SaaS and consumer products, often bringing domain-specific experience into more established markets. Developer tools and infrastructure attract all age groups.',
+ stats: [
+ { percent: 82, label: 'Founders under 30 building in AI/ML' },
+ { percent: 60, label: 'Startups building for end consumers' },
+ { percent: 16, label: 'Startups building for developers' },
+ ],
+ charts: ['IndustryChart'],
+
+ wordCloud: undefined,
+ summarizedAnswer: {
+ label: 'Problems startups are solving',
+ answers: [
+ 'AI-powered productivity tools',
+ 'Agent workflows (internal or customer-facing)',
+ 'Career preparation and job search',
+ 'AI copilots for small businesses',
+ 'AI-enhanced education and tutoring',
+ 'Tools for solopreneurs and creators',
+ 'Developer experience and API abstraction',
+ 'Sales and outreach automation',
+ 'Healthcare access and diagnostics',
+ 'Financial planning and forecasting',
+ 'Sustainability and climate data',
+ 'Privacy and compliance automation',
+ 'Time management and prioritization',
+ 'Collaboration and communication',
+ 'Mental health and wellness tracking',
+ ],
+ },
+ rankedAnswersPair: undefined,
+ },
+ {
+ title: 'Traction and Early Growth',
+ description:
+ 'One in five startups joined an accelerator. Y Combinator is the most common choice, especially in North America. Elsewhere, participation was more evenly distributed. Pivoting remains the norm, and less than half of startups are monetizing today.',
+ stats: [
+ { percent: 64, label: 'Startups that are pre-revenue' },
+ { percent: 59, label: 'Startups that pivoted at least once' },
+ { percent: 19, label: 'Startups that joined accelerators' },
+ ],
+ charts: ['AcceleratorParticipationChart'],
+ wordCloud: undefined,
+ summarizedAnswer: undefined,
+ rankedAnswersPair: undefined,
+ },
+ ],
+ },
+ {
+ title: 'What’s in a Startup’s Tech Stack',
+ shortTitle: 'Tech Stack',
+ description:
+ 'The modern stack centers around open tools, modular infrastructure, and cautious spending.',
+ pullQuote: {
+ quote:
+ 'Cursor has been my favourite tool so far. It’s made my life easier by documenting code on my behalf.',
+ author: 'Kevinton B',
+ authorPosition: 'Engineer, FlutterFlow',
+ authorAvatar: '/images/state-of-startups/quote-avatars/kevinton-b-120x120.jpg',
+ },
+ sections: [
+ {
+ title: 'Frameworks and Cloud Infra',
+ description:
+ 'Supabase and Postgres dominate backend infrastructure. React and Node top frontend and backend respectively. Cursor, Claude, and VS Code lead AI-assisted development. Developer tools like GitHub, Stripe, and Postman round out the stack.',
+ stats: [
+ {
+ percent: 83,
+ label: 'Startups with a JavaScript framework in their frontend stack',
+ },
+ {
+ percent: 62,
+ label: 'Startups with Supabase in their cloud provider stack',
+ },
+ { percent: 60, label: 'Startups with Node.js in their backend stack' },
+ ],
+ charts: ['DatabasesChart'],
+
+ wordCloud: undefined,
+ summarizedAnswer: undefined,
+ rankedAnswersPair: undefined,
+ },
+ {
+ title: 'Dev Tools and Time Savers',
+ description:
+ 'AI coding tools are indispensable for startups, and not just Cursor and Visual Studio Code. ‘Vibe coding’ tools like Loveable, Bolt.new, and v0 are also common.',
+ stats: [
+ { percent: 57, label: 'Startups that pay for OpenAI or ChatGPT' },
+ { percent: 37, label: 'Startups that pay for Cursor' },
+ { percent: 12, label: 'Startups that don’t pay for AI tools at all' },
+ ],
+ charts: ['AICodingToolsChart'],
+ wordCloud: {
+ label: 'Must-have developer tools by keyword frequency',
+ words: [
+ { text: 'cursor', count: 495 },
+ { text: 'code', count: 396 },
+ { text: 'supabase', count: 302 },
+ { text: 'github', count: 272 },
+ { text: 'vscode', count: 160 },
+ { text: 'claude', count: 143 },
+ { text: 'docker', count: 114 },
+ { text: 'git', count: 113 },
+ { text: 'studio', count: 112 },
+ { text: 'chatgpt', count: 92 },
+ { text: 'postman', count: 89 },
+ { text: 'copilot', count: 88 },
+ { text: 'lovable', count: 83 },
+ { text: 'visual', count: 76 },
+ { text: 'vercel', count: 71 },
+ { text: 'react', count: 70 },
+ { text: 'figma', count: 68 },
+ { text: 'ide', count: 58 },
+ { text: 'windsurf', count: 51 },
+ { text: 'backend', count: 44 },
+ { text: 'api', count: 40 },
+ { text: 'google', count: 37 },
+ { text: 'testing', count: 36 },
+ { text: 'tailwind', count: 35 },
+ { text: 'chrome', count: 34 },
+ { text: 'typescript', count: 33 },
+ { text: 'control', count: 33 },
+ { text: 'python', count: 32 },
+ { text: 'gemini', count: 30 },
+ ],
+ },
+ summarizedAnswer: {
+ label: 'Tools that startups wish existed',
+ answers: [
+ 'Unified backend platform combining auth, edge, database, and queues',
+ 'AI agents with real memory and workflow context',
+ 'Local-first dev environments that sync to Supabase or Git',
+ 'AI copilots for sales, marketing, or documentation',
+ 'UI builders with direct-to-code export and stateful logic',
+ 'Better CLI-driven or REPL-native dev tools',
+ 'Automated integration layers between SaaS APIs',
+ 'Real-time dashboards that don’t require BI tools',
+ 'Supabase + Neon or PlanetScale seamless sync',
+ 'One-click staging, testing, and preview environments',
+ 'AI validators for production database migrations',
+ 'Visual version control and state inspection for app logic',
+ 'Time-aware tools (versioned environments, snapshots, undoable infra)',
+ 'GPT-based toolchain composers (meta-dev agents)',
+ 'Agent-like task runners for cron jobs, workflows, monitoring',
+ ],
+ },
+ rankedAnswersPair: undefined,
+ },
+ ],
+ },
+ {
+ title: 'How Startups are Integrating AI',
+ shortTitle: 'AI and Agents',
+ description:
+ 'AI is a core product capability, not an afterthought. Most teams are using models like OpenAI or Claude for real features, not just demos.',
+ pullQuote: {
+ quote:
+ 'AI is embedded in how we build and scale. From using Claude and Cursor in dev, to voice AI in product for smarter, faster recruiting (which is our business).',
+ author: 'Jinal Jhaveri',
+ authorPosition: 'Founder, Mismo',
+ authorAvatar: '/images/state-of-startups/quote-avatars/jinal-j-120x120.jpg',
+ },
+ sections: [
+ {
+ title: 'In-Product AI Use',
+ description:
+ 'Most startups are already integrating models like OpenAI or Claude, especially for semantic search, summarisation, and customer support. Half are building agents to automate real tasks, from onboarding flows to sales triage.',
+ stats: [
+ {
+ percent: 81,
+ label: 'Startups using AI in their product',
+ },
+ { percent: 50, label: 'Startups building agents within their product' },
+ {
+ percent: 34,
+ label: 'Startups with agents automating customer support',
+ },
+ ],
+ charts: ['AIModelsChart'],
+ wordCloud: undefined,
+ summarizedAnswer: {
+ label: 'Most important AI use cases in product',
+ answers: [
+ 'Summarization / content generation',
+ 'Recommendations / personalization',
+ 'Workflow / agent-based automation',
+ 'Search / semantic search',
+ 'Customer support automation',
+ ],
+ },
+ rankedAnswersPair: undefined,
+ },
+ ],
+ },
+ {
+ title: 'Where Startups Go to Learn',
+ shortTitle: 'Influence',
+ description: 'Online communities are the learning engine behind every early-stage startup.',
+ pullQuote: undefined,
+ sections: [
+ {
+ title: 'Online Communities',
+ description:
+ 'There is a healthy diaspora of important online communities. That said, many people just lurk; few actively contribute to the discussion.',
+ stats: [
+ { percent: 55, label: 'Engineers using LinkedIn regularly' },
+ { percent: 45, label: 'Founders using X (Twitter) regularly' },
+ { percent: 7, label: 'Respondents that don’t use social media at all' },
+ ],
+ charts: ['RegularSocialMediaUseChart'],
+ wordCloud: undefined,
+ summarizedAnswer: undefined,
+ rankedAnswersPair: undefined,
+ },
+ {
+ title: 'Inspiration Stack',
+ description:
+ 'Founders follow newsletters like TLDR and Lenny’s, and they listen to podcasts like The Diary of a CEO and Founders. Tool discovery happens quite often via YouTube or GitHub. Physical event participation remains low.',
+ stats: [
+ { percent: 47, label: 'Respondents that listen to industry podcasts' },
+ {
+ percent: 20,
+ label: 'Respondents that subscribe to industry newsletters',
+ },
+ { percent: 12, label: 'Founders that have built a developer community' },
+ ],
+ charts: ['NewIdeasChart'],
+ wordCloud: undefined,
+ summarizedAnswer: undefined,
+ rankedAnswersPair: [
+ {
+ label: 'Top podcasts listened to',
+ answers: ['The Diary of a CEO', 'Founders', 'My First Million'],
+ },
+ {
+ label: 'Top newsletters subscribed to',
+ answers: ['TLDR', 'Lenny’s Newsletter', 'The Pragmatic Engineer'],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ title: 'How Startups are Finding Customers',
+ shortTitle: 'Go-To-Market',
+ description:
+ 'Startups start selling through their networks and dev communities. Only when they grow do they layer in more structured growth via CRMs and sales.',
+ pullQuote: undefined,
+ sections: [
+ {
+ title: 'Initial Customers',
+ description:
+ 'Founders earn their earliest customers through networks, communities, and inbound content. Paid acquisition rarely works early on, nor does performance marketing.',
+ stats: [
+ {
+ percent: 58,
+ label:
+ 'Startups that get their first customers via personal and professional networks',
+ },
+ { percent: 48, label: 'Startups that engage users through social media' },
+ { percent: 39, label: 'Startups that are still experimenting with pricing models' },
+ ],
+ charts: ['InitialPayingCustomersChart'],
+ wordCloud: undefined,
+ summarizedAnswer: undefined,
+ rankedAnswersPair: undefined,
+ },
+ {
+ title: 'Founder-led Sales',
+ description:
+ 'Sales is still founder-led at most startups. Dedicated sales hires usually don’t arrive until after 10+ employees. Many still use Google Sheets or nothing at all to track sales activity.',
+ stats: [
+ {
+ percent: 75,
+ label: 'Startups with their founders still directly responsible for sales',
+ },
+ {
+ percent: 58,
+ label: 'Startups that got their initial customers from personal networks',
+ },
+ { percent: 7, label: 'Startups that have a dedicated sales team' },
+ ],
+ charts: ['SalesToolsChart'],
+ wordCloud: undefined,
+ summarizedAnswer: undefined,
+ rankedAnswersPair: undefined,
+ },
+ ],
+ },
+ {
+ title: 'Biggest Challenges for Startups',
+ shortTitle: 'Outlook',
+ description:
+ 'Startups remain optimistic about the future but are weighed down by technical complexity, customer acquisition hurdles, and a wish list of tools that still don’t exist.',
+ pullQuote: {
+ quote:
+ 'There’s plenty of uncertainty, but we’re building something that feels deeply worth it. That gives us a lot of confidence in the long run.',
+ author: 'Robert Wolski',
+ authorPosition: 'Founder, Keepsake',
+ authorAvatar: '/images/state-of-startups/quote-avatars/robert-w-120x120.jpg',
+ },
+ sections: [
+ {
+ title: 'The Road Ahead',
+ description:
+ 'The hardest problems are still the oldest ones: customer acquisition, product-market fit, and complexity. Startups cite AI-assisted coding and backend services as major time-savers, but many are still missing critical tools they want. Especially around onboarding, dashboards, and agents.',
+ stats: [
+ {
+ percent: 82,
+ label: 'Founders that evaluate tools via hands-on experience',
+ },
+ {
+ percent: 45,
+ label:
+ 'Startups with over 250 employees whose biggest challenge is getting customers',
+ },
+ {
+ percent: 4,
+ label: 'Startups with under 10 employees whose biggest challenge is hiring',
+ },
+ ],
+ charts: ['BiggestChallengeChart'],
+
+ wordCloud: undefined,
+ summarizedAnswer: undefined,
+ rankedAnswersPair: undefined,
+ },
+ {
+ title: 'Worldview and Optimism',
+ description:
+ 'Most startup founders remain upbeat about the future, but that confidence isn’t shared equally. Engineers and marketers show more caution.',
+
+ stats: [
+ { percent: 61, label: 'Founders that are optimistic' },
+ { percent: 50, label: 'Engineers that are optimistic' },
+ { percent: 42, label: 'Other roles that are optimistic' },
+ ],
+ charts: ['WorldOutlookChart'],
+ wordCloud: undefined,
+ summarizedAnswer: undefined,
+ rankedAnswersPair: undefined,
+ },
+ ],
+ },
+ ],
+ participantsList: [
+ {
+ company: 'Wasp',
+ url: 'https://wasp.sh/',
+ },
+ {
+ company: 'Greptile',
+ url: 'https://www.greptile.com/',
+ },
+ {
+ company: 'Zaymo',
+ url: 'https://zaymo.com',
+ },
+ {
+ company: 'Jazzberry',
+ url: 'https://jazzberry.ai',
+ },
+ {
+ company: 'Shor',
+ url: 'https://tryshor.com',
+ },
+
+ {
+ company: 'Mono',
+ url: 'https://www.mono.la',
+ },
+ {
+ company: 'Affl.ai',
+ url: 'https://www.affil.ai',
+ },
+ {
+ company: 'Docsum',
+ url: 'https://www.docsum.ai/',
+ },
+ {
+ company: 'Hazel',
+ url: 'https://hazelai.com/',
+ },
+ {
+ company: 'Rivet',
+ url: 'https://rivet.gg/',
+ },
+ {
+ company: 'Trieve',
+ url: 'https://trieve.ai/',
+ },
+ {
+ company: 'Artificial Societies',
+ url: 'https://societies.io/',
+ },
+ {
+ company: 'Gauge',
+ url: 'https://withgauge.com/',
+ },
+ {
+ company: 'Stardex',
+ url: 'https://www.stardex.com/',
+ },
+ {
+ company: 'TrueClaim',
+ url: 'https://www.trytrueclaim.com/',
+ },
+ {
+ company: 'Autosana',
+ url: 'https://autosana.ai/',
+ },
+ {
+ company: 'Vespper',
+ url: 'https://vespper.com/',
+ },
+ {
+ company: 'Curo',
+ url: 'https://www.curocharging.com/',
+ },
+ {
+ company: 'Kombo',
+ url: 'https://kombo.dev/',
+ },
+ {
+ company: 'Candle',
+ url: 'https://www.trycandle.app/',
+ },
+ {
+ company: 'Trainloop',
+ url: 'http://trainloop.ai/',
+ },
+ {
+ company: 'Replit',
+ url: 'https://replit.com/',
+ },
+ {
+ company: 'Roe AI',
+ url: 'https://getroe.ai/',
+ },
+ {
+ company: 'Kestral',
+ url: 'https://kestral.team/',
+ },
+ {
+ company: 'Revyl',
+ url: 'https://www.revyl.ai/',
+ },
+ {
+ company: 'Arva AI',
+ url: 'https://www.arva.ai/',
+ },
+ {
+ company: 'Posthog',
+ url: 'https://www.posthog.com',
+ },
+ {
+ company: 'Rootly',
+ url: 'https://rootly.com/',
+ },
+ {
+ company: 'Throxy',
+ url: 'https://throxy.com/',
+ },
+ {
+ company: 'Zapi',
+ url: 'https://heyzapi.com/',
+ },
+ {
+ company: 'Leaping AI',
+ url: 'https://www.leapingai.com/',
+ },
+ {
+ company: 'WarpBuild',
+ url: 'https://www.warpbuild.com/',
+ },
+ {
+ company: 'Domu',
+ url: 'https://www.domu.ai/',
+ },
+ {
+ company: 'Bilanc',
+ url: 'https://www.bilanc.co/',
+ },
+ {
+ company: 'Miru',
+ url: 'https://www.miruml.com/',
+ },
+ {
+ company: 'Repaint',
+ url: 'https://repaint.com/',
+ },
+ {
+ company: 'Cubic',
+ url: 'https://cubic.dev/',
+ },
+ {
+ company: 'CTGT',
+ url: 'https://www.ctgt.ai/',
+ },
+ {
+ company: 'Integrated Reasoning',
+ url: 'https://integrated-reasoning.com/',
+ },
+ {
+ company: 'Datafruit',
+ url: 'https://datafruit.dev/',
+ },
+ {
+ company: 'mcp-use',
+ url: 'https://mcp-use.com/',
+ },
+ {
+ company: 'Weave',
+ url: 'http://getweave.com/',
+ },
+ {
+ company: 'Palmier',
+ url: 'https://www.palmier.io/',
+ },
+ {
+ company: 'Rainmaker',
+ url: 'http://www.rainmaker.nyc/',
+ },
+ {
+ company: 'Wasmer',
+ url: 'https://wasmer.io/',
+ },
+ {
+ company: 'Artie',
+ url: 'https://www.artie.com/',
+ },
+ {
+ company: 'Lumari',
+ url: 'https://www.lumari.io/',
+ },
+ {
+ company: 'Hitpay',
+ url: 'https://www.hitpayapp.com/',
+ },
+ {
+ company: 'Tempo',
+ url: 'https://www.tempo.new/',
+ },
+ {
+ company: 'SalesPatriot',
+ url: 'https://www.salespatriot.com/',
+ },
+ {
+ company: 'Surge',
+ url: 'https://surge.app/',
+ },
+ {
+ company: 'Linum',
+ url: 'https://www.linum.ai/',
+ },
+ {
+ company: 'Rebill',
+ url: 'https://www.rebill.com/',
+ },
+ {
+ company: 'Careswift',
+ url: 'https://www.careswift.com/',
+ },
+ {
+ company: 'Autumn',
+ url: 'https://useautumn.com/',
+ },
+ {
+ company: 'Rollstack',
+ url: 'https://www.rollstack.com/',
+ },
+ {
+ company: 'OpenNote',
+ url: 'https://opennote.com/',
+ },
+ {
+ company: 'Coverage Cat',
+ url: 'https://www.coveragecat.com/',
+ },
+ {
+ company: 'Flair Labs',
+ url: 'https://www.flairlabs.ai/',
+ },
+ {
+ company: 'Percival',
+ url: 'https://www.percivaltech.com/',
+ },
+ {
+ company: 'Morphik',
+ url: 'https://morphik.ai/',
+ },
+ {
+ company: 'DrDroid',
+ url: 'https://drdroid.io/',
+ },
+ {
+ company: 'Nautilus',
+ url: 'https://nautilus.co/',
+ },
+ ],
+}
+
+export default stateOfStartupsData
diff --git a/apps/www/data/tweets/Tweets.json b/apps/www/data/tweets/Tweets.json
deleted file mode 100644
index e678bf7e233c1..0000000000000
--- a/apps/www/data/tweets/Tweets.json
+++ /dev/null
@@ -1,230 +0,0 @@
-[
- {
- "text": "Working with @supabase has been one of the best dev experiences I've had lately. Incredibly easy to set up, great documentation, and so many fewer hoops to jump through than the competition. I definitely plan to use it on any and all future projects.",
- "url": "https://twitter.com/thatguy_tex/status/1497602628410388480",
- "handle": "thatguy_tex",
- "img_url": "/images/twitter-profiles/09HouOSt_400x400.jpg"
- },
- {
- "text": "@supabase is just 🤯 Now I see why a lot of people love using it as a backend for their applications. I am really impressed with how easy it is to set up an Auth and then just code it together for the frontend. @IngoKpp now I see your joy with Supabase #coding #fullstackwebdev",
- "url": "https://twitter.com/IxoyeDesign/status/1497473731777728512",
- "handle": "IxoyeDesign",
- "img_url": "/images/twitter-profiles/C8opIL-g_400x400.jpg"
- },
- {
- "text": "I've been using @supabase for two personal projects and it has been amazing being able to use the power of Postgres and don't have to worry about the backend",
- "url": "https://twitter.com/varlenneto/status/1496595780475535366",
- "handle": "varlenneto",
- "img_url": "/images/twitter-profiles/wkXN0t_F_400x400.jpg"
- },
- {
- "text": "Y'all @supabase + @nextjs is amazing! 🙌 Barely an hour into a proof-of-concept and already have most of the functionality in place. 🤯🤯🤯",
- "url": "https://twitter.com/justinjunodev/status/1500264302749622273",
- "handle": "justinjunodev",
- "img_url": "/images/twitter-profiles/9k_ZB9OO_400x400.jpg"
- },
- {
- "text": "And thanks to @supabase, I was able to go from idea to launched feature in a matter of hours. Absolutely amazing!",
- "url": "https://twitter.com/BraydonCoyer/status/1511071369731137537",
- "handle": "BraydonCoyer",
- "img_url": "/images/twitter-profiles/8YxkpW8f_400x400.jpg"
- },
- {
- "text": "Contributing to open-source projects and seeing merged PRs gives enormous happiness! Special thanks to @supabase, for giving this opportunity by staying open-source and being junior-friendly✌🏼",
- "url": "https://twitter.com/damlakoksal/status/1511436907984662539",
- "handle": "damlakoksal",
- "img_url": "/images/twitter-profiles/N8EfTFs7_400x400.jpg"
- },
- {
- "text": "Holy crap. @supabase is absolutely incredible. Most elegant backend as a service I've ever used. This is a dream.",
- "url": "https://twitter.com/kentherogers/status/1512609587110719488",
- "handle": "KenTheRogers",
- "img_url": "/images/twitter-profiles/9l9Td-Fz_400x400.jpg"
- },
- {
- "text": "Using @supabase I'm really pleased on the power of postgres (and sql in general). Despite being a bit dubious about the whole backend as a service thing I have to say I really don't miss anything. The whole experience feel very robust and secure.",
- "url": "https://twitter.com/paoloricciuti/status/1497691838597066752",
- "handle": "PaoloRicciuti",
- "img_url": "/images/twitter-profiles/OCDKFUOp_400x400.jpg"
- },
- {
- "text": "@supabase is lit. It took me less than 10 minutes to setup, the DX is just amazing.",
- "url": "https://twitter.com/saxxone/status/1500812171063828486",
- "handle": "saxxone",
- "img_url": "/images/twitter-profiles/BXi6z1M7_400x400.jpg"
- },
- {
- "text": "I’m not sure what magic @supabase is using but we’ve migrated @happyteamsdotio database to @supabase from @heroku and it’s much much faster at half the cost.",
- "url": "https://twitter.com/michaelcdever/status/1524753565599690754",
- "handle": "michaelcdever",
- "img_url": "/images/twitter-profiles/rWX8Jzp5_400x400.jpg"
- },
- {
- "text": "There are a lot of indie hackers building in public, but it’s rare to see a startup shipping as consistently and transparently as Supabase. Their upcoming March releases look to be 🔥 Def worth a follow! also opened my eyes as to how to value add in open source.",
- "url": "https://twitter.com/swyx/status/1366685025047994373",
- "handle": "swyx",
- "img_url": "/images/twitter-profiles/qhvO9V6x_400x400.jpg"
- },
- {
- "text": "This weekend I made a personal record 🥇 on the less time spent creating an application with social login / permissions, database, cdn, infinite scaling, git push to deploy and for free. Thanks to @supabase and @vercel",
- "url": "https://twitter.com/jperelli/status/1366195769657720834",
- "handle": "jperelli",
- "img_url": "/images/twitter-profiles/_ki30kYo_400x400.jpg"
- },
- {
- "text": "Badass! Supabase is amazing. literally saves our small team a whole engineer’s worth of work constantly. The founders and everyone I’ve chatted with at supabase are just awesome people as well :)",
- "url": "https://twitter.com/KennethCassel/status/1524359528619384834",
- "handle": "KennethCassel",
- "img_url": "/images/twitter-profiles/pmQj3TX-_400x400.jpg"
- },
- {
- "text": "Working with Supabase is just fun. It makes working with a DB so much easier.",
- "url": "https://twitter.com/the_BrianB/status/1524716498442276864",
- "handle": "the_BrianB",
- "img_url": "/images/twitter-profiles/7NITI8Z3_400x400.jpg"
- },
- {
- "text": "This community is STRONG and will continue to be the reason why developers flock to @supabase over an alternative. Keep up the good work! ⚡️",
- "url": "https://twitter.com/_wilhelm__/status/1524074865107488769",
- "handle": "_wilhelm__",
- "img_url": "/images/twitter-profiles/CvqDy6YF_400x400.jpg"
- },
- {
- "text": "Working on my next SaaS app and I want this to be my whole job because I'm just straight out vibing putting it together. @supabase and chill, if you will",
- "url": "https://twitter.com/drewclemcr8/status/1523843155484942340",
- "handle": "drewclemcr8",
- "img_url": "/images/twitter-profiles/bJlKtSxz_400x400.jpg"
- },
- {
- "text": "@supabase Putting a ton of well-explained example API queries in a self-building documentation is just a classy move all around. I also love having GraphQL-style nested queries with traditional SQL filtering. This is pure DX delight. A+++. #backend",
- "url": "https://twitter.com/CodiferousCoder/status/1522233113207836675",
- "handle": "CodiferousCoder",
- "img_url": "/images/twitter-profiles/t37cVLwy_400x400.jpg"
- },
- {
- "text": "Me using @supabase for the first time right now 🤯",
- "url": "https://twitter.com/nasiscoe/status/1365140856035024902",
- "handle": "nasiscoe",
- "img_url": "/images/twitter-profiles/nc2Ms5hH_400x400.jpg"
- },
- {
- "text": "Check out this amazing product @supabase. A must give try #newidea #opportunity",
- "url": "https://twitter.com/digitaldaswani/status/1364447219642814464",
- "handle": "digitaldaswani",
- "img_url": "/images/twitter-profiles/w8HLdlC7_400x400.jpg"
- },
- {
- "text": "I gave @supabase a try this weekend and I was able to create a quick dashboard to visualize the data from the PostgreSQL instance. It's super easy to use Supabase's API or the direct DB connection. Check out the tutorial 📖",
- "url": "https://twitter.com/razvanilin/status/1363770020581412867",
- "handle": "razvanilin",
- "img_url": "/images/twitter-profiles/AiaH9vJ2_400x400.jpg"
- },
- {
- "text": "Tried @supabase for the first time yesterday. Amazing tool! I was able to get my Posgres DB up in no time and their documentation on operating on the DB is super easy! 👏 Can't wait for Cloud functions to arrive! It's gonna be a great Firebase alternative!",
- "url": "https://twitter.com/chinchang457/status/1363347740793524227",
- "handle": "chinchang457",
- "img_url": "/images/twitter-profiles/LTw5OCnv_400x400.jpg"
- },
- {
- "text": "I gave @supabase a try today and I was positively impressed! Very quick setup to get a working remote database with API access and documentation generated automatically for you 👌 10/10 will play more",
- "url": "https://twitter.com/razvanilin/status/1363002398738800640",
- "handle": "razvanilin",
- "img_url": "/images/twitter-profiles/AiaH9vJ2_400x400.jpg"
- },
- {
- "text": "Wait. Is it so easy to write queries for @supabase ? It's like simple SQL stuff!",
- "url": "https://twitter.com/T0ny_Boy/status/1362911838908911617",
- "handle": "T0ny_Boy",
- "img_url": "/images/twitter-profiles/UCBhUBZl_400x400.jpg"
- },
- {
- "text": "Jeez, and @supabase have native support for magic link login?! I was going to use http://magic.link for this But if I can get my whole DB + auth + magic link support in one... Awesome",
- "url": "https://twitter.com/louisbarclay/status/1362016666868154371",
- "handle": "louisbarclay",
- "img_url": "/images/twitter-profiles/6f1O8ZOW_400x400.jpg"
- },
- {
- "text": "@MongoDB or @MySQL?!?! Please, let me introduce you to @supabase and the wonderful world of @PostgreSQL before it's too late!!",
- "url": "https://twitter.com/jim_bisenius/status/1361772978841788416",
- "handle": "jim_bisenius",
- "img_url": "/images/twitter-profiles/rLgwUZSB_400x400.jpg"
- },
- {
- "text": "Where has @supabase been all my life? 😍",
- "url": "https://twitter.com/Elsolo244/status/1360257201911320579",
- "handle": "Elsolo244",
- "img_url": "/images/twitter-profiles/v6citnk33y2wpeyzrq05_400x400.jpeg"
- },
- {
- "text": "I think you'll love @supabase :-) Open-source, PostgreSQL-based & zero magic.",
- "url": "https://twitter.com/zippoxer/status/1360021315852328961",
- "handle": "zippoxer",
- "img_url": "/images/twitter-profiles/6rd3xub9_400x400.png"
- },
- {
- "text": "@supabase is the answer to all of firebase’s problems imo",
- "url": "https://twitter.com/jim_bisenius/status/1358590362953142278",
- "handle": "jim_bisenius",
- "img_url": "/images/twitter-profiles/rLgwUZSB_400x400.jpg"
- },
- {
- "text": "@supabase is insane.",
- "url": "https://twitter.com/codewithbhargav/status/1357647840911126528",
- "handle": "codewithbhargav",
- "img_url": "/images/twitter-profiles/LQYfHXBp_400x400.jpg"
- },
- {
- "text": "It’s fun, feels lightweight, and really quick to spin up user auth and a few tables. Almost too easy! Highly recommend.",
- "url": "https://twitter.com/nerdburn/status/1356857261495214085",
- "handle": "nerdburn",
- "img_url": "/images/twitter-profiles/66VSV9Mm_400x400.png"
- },
- {
- "text": "I’m probably the wrong person to ask because I pick tools based on UX. Supabase was immediately approachable: instant setup, fast web app, auth, and easy APIs. Same reason I liked Firebase when I first discovered.",
- "url": "https://twitter.com/jasonbarone/status/1357015483619422210",
- "handle": "jasonbarone",
- "img_url": "/images/twitter-profiles/6zCnwpvi_400x400.jpg"
- },
- {
- "text": "Now things are starting to get interesting! Firebase has long been the obvious choice for many #flutter devs for the ease of use. But their databases are NoSQL, which has its downsides... Seems like @supabase is working on something interesting here!",
- "url": "https://twitter.com/RobertBrunhage/status/1356973695865085953",
- "handle": "RobertBrunhage",
- "img_url": "/images/twitter-profiles/5LMWEACf_400x400.jpg"
- },
- {
- "text": "Honestly, I really love what @supabase is doing, you don't need to own a complete backend, just write your logic within your app and you'll get a powerful Postgresql at your disposal.",
- "url": "https://twitter.com/NavicsteinR/status/1356927229217959941",
- "handle": "NavicsteinR",
- "img_url": "/images/twitter-profiles/w_zNZAs7_400x400.jpg"
- },
- {
- "text": "Next.js, @supabase, @stripe, and @vercel. Supastack™",
- "url": "https://twitter.com/jasonbarone/status/1356765411832922115",
- "handle": "jasonbarone",
- "img_url": "/images/twitter-profiles/6zCnwpvi_400x400.jpg"
- },
- {
- "text": "I've really enjoyed the DX! Extremely fun to use, which is odd to say about a database at least for me.",
- "url": "https://twitter.com/Soham_Asmi/status/1373086068132745217",
- "handle": "Soham_Asmi",
- "img_url": "/images/twitter-profiles/Os4nhKIr_400x400.jpg"
- },
- {
- "text": "Supabase team is doing some awesome stuff #supabase #facts @supabase",
- "url": "https://twitter.com/_strawbird/status/1372607500499841025",
- "handle": "_strawbird",
- "img_url": "/images/twitter-profiles/iMBvvQdn_400x400.jpg"
- },
- {
- "text": "Did a website with @supabase last week with no prior experience with it. Up and running in 20 minutes. It's awesome to use. Thumbs up",
- "url": "https://twitter.com/michael_webdev/status/1352885366928404481?s=20",
- "handle": "michael_webdev",
- "img_url": "/images/twitter-profiles/SvAyLaWV_400x400.jpg"
- },
- {
- "text": "Next.js, @supabase, @stripe, and @vercel. Supastack™",
- "url": "https://twitter.com/jasonbarone/status/1356765411832922115?s=20",
- "handle": "jasonbarone",
- "img_url": "/images/twitter-profiles/6zCnwpvi_400x400.jpg"
- }
-]
diff --git a/apps/www/pages/state-of-startups.tsx b/apps/www/pages/state-of-startups.tsx
index 7a57200e5899d..81b9e0221057d 100644
--- a/apps/www/pages/state-of-startups.tsx
+++ b/apps/www/pages/state-of-startups.tsx
@@ -1,164 +1,189 @@
-import { useEffect, useRef, useState } from 'react'
-import { animate, createSpring, createTimeline, stagger } from 'animejs'
-import Link from 'next/link'
-import Image from 'next/image'
-import { NextSeo } from 'next-seo'
-
-import { Button, Checkbox, cn } from 'ui'
-import { PopupFrame } from 'ui-patterns'
-import { Input } from 'ui/src/components/shadcn/ui/input'
-import { Label } from 'ui/src/components/shadcn/ui/label'
-import DefaultLayout from '~/components/Layouts/Default'
-import SectionContainer from '~/components/Layouts/SectionContainer'
-import { useSendTelemetryEvent } from '~/lib/telemetry'
-
-import data from '~/data/surveys/state-of-startups-2025'
-
-interface FormData {
- email: string
- terms: boolean
-}
+import { useRouter } from 'next/router'
+import { forwardRef, useEffect, useRef, useState } from 'react'
-interface FormItem {
- type: 'email' | 'checkbox'
- label: string
- placeholder: string
- required: boolean
- className?: string
- component: typeof Input | typeof Checkbox
-}
+import { NextSeo } from 'next-seo'
+import Link from 'next/link'
-type FormConfig = {
- [K in keyof FormData]: FormItem
-}
+import { motion } from 'framer-motion'
+import { Button, cn } from 'ui'
-const formConfig: FormConfig = {
- email: {
- type: 'email',
- label: 'Email',
- placeholder: 'Email',
- required: true,
- className: '',
- component: Input,
- },
- terms: {
- type: 'checkbox',
- label: '',
- placeholder: '',
- required: true,
- className: '',
- component: Checkbox,
- },
-}
+import { useFlag } from 'common'
+import DefaultLayout from '~/components/Layouts/Default'
+import { DecorativeProgressBar } from '~/components/SurveyResults/DecorativeProgressBar'
+import { SurveyChapter } from '~/components/SurveyResults/SurveyChapter'
+import { SurveyChapterSection } from '~/components/SurveyResults/SurveyChapterSection'
+import { SurveySectionBreak } from '~/components/SurveyResults/SurveySectionBreak'
-const defaultFormValue: FormData = {
- email: '',
- terms: false,
-}
+import { useSendTelemetryEvent } from '~/lib/telemetry'
-const isValidEmail = (email: string): boolean => {
- const emailPattern = /^[\w-\.+]+@([\w-]+\.)+[\w-]{2,8}$/
- return emailPattern.test(email)
-}
+import pageData from '~/data/surveys/state-of-startups-2025'
function StateOfStartupsPage() {
- const pageData = data()
- const meta_title = pageData.metaTitle
- const meta_description = pageData.metaDescription
- const meta_image = pageData.metaImage
-
- const [formData, setFormData] = useState(defaultFormValue)
- const [honeypot, setHoneypot] = useState('') // field to prevent spam
- const [errors, setErrors] = useState<{ [key: string]: string }>({})
- const [isSubmitting, setIsSubmitting] = useState(false)
- const [success, setSuccess] = useState(null)
- const [startTime, setStartTime] = useState(0)
+ const router = useRouter()
+ const isPageEnabled = useFlag('stateOfStartups')
- const sendTelemetryEvent = useSendTelemetryEvent()
+ const meta_title = pageData.metaTitle || 'State of Startups 2025 | Supabase'
+ const meta_description =
+ pageData.metaDescription ||
+ 'We surveyed over 2,000 startup founders and builders to uncover what’s powering modern startups: their stacks, their go-to-market motion, and their approach to AI.'
- const handleChange = (e: React.ChangeEvent) => {
- const { name, value } = e.target
- setErrors({})
- setFormData((prev) => ({ ...prev, [name]: value }))
- }
-
- const handleReset = (e: React.MouseEvent) => {
- e.preventDefault()
- setFormData(defaultFormValue)
- setSuccess(null)
- setErrors({})
- }
+ const [showFloatingToc, setShowFloatingToc] = useState(false)
+ const [isTocOpen, setIsTocOpen] = useState(false)
+ const [activeChapter, setActiveChapter] = useState(1)
+ const tocRef = useRef(null)
+ const heroRef = useRef(null)
+ const ctaBannerRef = useRef(null)
- const validate = (): boolean => {
- const newErrors: { [key in keyof FormData]?: string } = {}
+ useEffect(() => {
+ if (isPageEnabled !== undefined && !isPageEnabled) router.push('/')
+ }, [isPageEnabled, router])
- // Check required fields
- for (const key in formConfig) {
- if (formConfig[key as keyof FormData].required && !formData[key as keyof FormData]) {
- if (key === 'email') {
- newErrors[key as keyof FormData] = `Email is required`
- } else if (key === 'terms') {
- newErrors[key as keyof FormData] = `You must agree to the terms`
+ // Scroll detection to show floating ToC
+ useEffect(() => {
+ const handleScroll = () => {
+ const heroElement = heroRef.current
+ const ctaBannerElement = ctaBannerRef.current
+
+ if (heroElement && ctaBannerElement) {
+ const heroRect = heroElement.getBoundingClientRect()
+ const ctaBannerRect = ctaBannerElement.getBoundingClientRect()
+
+ // Show floating ToC when the hero section is completely out of view
+ // but hide it when the CTA banner comes into view
+ if (heroRect.bottom < 0 && ctaBannerRect.top > window.innerHeight) {
+ setShowFloatingToc(true)
+ } else {
+ setShowFloatingToc(false)
+ setIsTocOpen(false)
}
}
}
- // Validate email
- if (formData.email && !isValidEmail(formData.email)) {
- newErrors.email = 'Invalid email address'
- }
-
- setErrors(newErrors)
+ window.addEventListener('scroll', handleScroll)
+ return () => window.removeEventListener('scroll', handleScroll)
+ }, [])
- // Return validation status, also check if honeypot is filled (indicating a bot)
- return Object.keys(newErrors).length === 0 && honeypot === ''
- }
+ // Active chapter detection
+ useEffect(() => {
+ const handleScroll = () => {
+ const chapters = pageData.pageChapters
+ const scrollY = window.scrollY + 100 // Offset for better detection
+
+ for (let i = chapters.length - 1; i >= 0; i--) {
+ const chapterElement = document.getElementById(`chapter-${i + 1}`)
+ if (chapterElement && scrollY >= chapterElement.offsetTop) {
+ setActiveChapter(i + 1)
+ break
+ }
+ }
+ }
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
+ window.addEventListener('scroll', handleScroll)
+ return () => window.removeEventListener('scroll', handleScroll)
+ }, [])
- const currentTime = Date.now()
- const timeElapsed = (currentTime - startTime) / 1000
+ // Close ToC when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ const isOutsideFloating = tocRef.current && !tocRef.current.contains(event.target as Node)
- // Spam prevention: Reject form if submitted too quickly (less than 3 seconds)
- if (timeElapsed < 3) {
- setErrors({ general: 'Submission too fast. Please fill the form correctly.' })
- return
+ if (isOutsideFloating) {
+ setIsTocOpen(false)
+ }
}
- if (!validate()) {
- return
+ if (isTocOpen) {
+ document.addEventListener('mousedown', handleClickOutside)
}
- setIsSubmitting(true)
- setSuccess(null)
-
- try {
- const response = await fetch('/api-v2/submit-form-sos2025-newsletter', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(formData),
- })
-
- if (response.ok) {
- setSuccess('Thank you for your submission!')
- setFormData({ email: '', terms: false })
- } else {
- const errorData = await response.json()
- setErrors({ general: `Submission failed: ${errorData.message}` })
- }
- } catch (error) {
- setErrors({ general: 'An unexpected error occurred. Please try again.' })
- } finally {
- setIsSubmitting(false)
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside)
}
+ }, [isTocOpen])
+
+ // Floating Table of Contents
+ const FloatingTableOfContents = () => {
+ const currentChapter = pageData.pageChapters[activeChapter - 1]
+
+ if (!showFloatingToc) return null
+
+ return (
+
+
+ {/* Closed ToC */}
+
setIsTocOpen(true)}
+ className={cn(
+ 'flex flex-row gap-2 shadow-xl rounded-full px-2 pr-4',
+ isTocOpen && 'hidden'
+ )}
+ >
+
+
+ {activeChapter}
+
+
+ {currentChapter?.shortTitle}
+
+
+
+
+ {/* Open ToC */}
+ {isTocOpen && (
+
+
+ {pageData.pageChapters.map((chapter, chapterIndex) => (
+
+ setIsTocOpen(false)}
+ className={cn(
+ 'block px-6 py-2 text-xs transition-colors font-mono uppercase tracking-wider text-center text-foreground-light hover:text-brand-link hover:bg-brand-300/25',
+ chapterIndex + 1 === activeChapter &&
+ 'bg-brand-300/40 text-brand-link dark:text-brand'
+ )}
+ >
+ {chapter.shortTitle}
+
+
+ ))}
+
+
+ )}
+
+
+ )
}
- useEffect(() => {
- setStartTime(Date.now())
- }, [])
+ if (!isPageEnabled) return null
return (
<>
@@ -171,364 +196,199 @@ function StateOfStartupsPage() {
url: `https://supabase.com/state-of-startups`,
images: [
{
- url: meta_image,
+ url: `https://supabase.com/images/state-of-startups/state-of-startups-og.png`,
},
],
}}
/>
-
-
-
-
-
Stay in touch
-
- Sign up for our newsletter to be notified when the survey results are available.
-
-
- {success ? (
-
- ) : (
-
-
+
+
+
+
+
+ {pageData.pageChapters.map((chapter, chapterIndex) => (
+
+ {chapter.sections.map((section, sectionIndex) => (
+
+ ))}
+
+ ))}
+
+
>
)
}
-const Hero = (props: any) => {
- const animRef = useRef
(null)
-
- useEffect(() => {
- if (!animRef.current) return
-
- const strings = [
- "What's your tech stack?",
- "What's your favorite AI developer tool?",
- 'Which vector database are you using?',
- 'Are you building AI Agents?',
- 'Do you use OpenTelemetry?',
- 'Where do you go to learn?',
- ]
-
- let currentIndex = 0
-
- const animateText = () => {
- const animatedText = animRef.current?.querySelector('#anim')
- if (!animatedText) return
-
- const currentText = strings[currentIndex]
-
- animatedText.textContent = currentText
- // Split by words and wrap each word, then wrap letters within each word
- animatedText.innerHTML = currentText
- .split(' ')
- .map((word) => {
- if (word.trim() === '') return ' '
- const wrappedLetters = word.replace(
- /\S/g,
- "$& "
- )
- return `${wrappedLetters} `
- })
- .join(' ')
-
- createTimeline({
- onComplete: () => {
- currentIndex = (currentIndex + 1) % strings.length
- setTimeout(() => {
- animateOut()
- }, 100)
- },
- }).add(animatedText.querySelectorAll('.letter'), {
- opacity: [0, 1],
- translateY: [-8, 0],
- ease: createSpring({ stiffness: 150, damping: 15 }),
- duration: 500,
- delay: stagger(10),
- })
- }
-
- const animateOut = () => {
- const animatedText = animRef.current?.querySelector('#anim')
- if (!animatedText) return
-
- animate(animatedText.querySelectorAll('.letter'), {
- opacity: [1, 0],
- translateY: [0, 8],
- ease: 'inExpo',
- duration: 500,
- delay: stagger(10),
- onComplete: () => {
- setTimeout(animateText, -100)
- },
- })
- }
+export default StateOfStartupsPage
- animateText()
+// Component for the participants list
+const ParticipantsList = () => {
+ const [shuffledParticipants, setShuffledParticipants] = useState(pageData.participantsList)
- return () => {}
+ useEffect(() => {
+ // Simple shuffle after mount, no animation because it's at the bottom of the page
+ const shuffled = [...pageData.participantsList].sort(() => Math.random() - 0.5)
+ setShuffledParticipants(shuffled)
}, [])
return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {props.icon || props.title ? (
-
- {props.title && (
-
- {props.title}
-
- )}
-
- ) : null}
-
-
-
-
- State of Startups
-
-
-
{props.subheader}
-
-
-
- Take the survey
-
- }
- className="[&_.modal-content]:min-h-[650px] [&_.modal-content]:!h-[75vh] [&_.modal-content]:flex [&_.modal-content]:flex-col"
- >
-
-
-
-
-
-
-
+
+
+
Thank you
+
+ A special thanks to the following companies for participating in this year’s survey.
+
- >
+
+
+ {shuffledParticipants.map((participant, index) => (
+
+
+ {participant.company}
+
+
+ ))}
+
+
)
}
-export default StateOfStartupsPage
+// Component for the 'Builders choose Supabase' CTA at the bottom of the page
+const CTABanner = forwardRef
((props, ref) => {
+ const sendTelemetryEvent = useSendTelemetryEvent()
+ return (
+
+
+
Builders choose Supabase
+
+ Supabase is the Postgres development platform. Build your startup with a Postgres
+ database, Authentication, instant APIs, Edge Functions, Realtime subscriptions, Storage,
+ and Vector embeddings.
+
+
+
+
+
+ sendTelemetryEvent({
+ action: 'start_project_button_clicked',
+ properties: { buttonLocation: 'CTA Banner' },
+ })
+ }
+ >
+ Start your project
+
+
+
+
+ sendTelemetryEvent({
+ action: 'request_demo_button_clicked',
+ properties: { buttonLocation: 'CTA Banner' },
+ })
+ }
+ >
+ Request a demo
+
+
+
+
+ )
+})
+
+CTABanner.displayName = 'CTABanner'
diff --git a/apps/www/public/images/state-of-startups/pattern-checker.svg b/apps/www/public/images/state-of-startups/pattern-checker.svg
new file mode 100644
index 0000000000000..eb7fdbfdde912
--- /dev/null
+++ b/apps/www/public/images/state-of-startups/pattern-checker.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/www/public/images/state-of-startups/pattern-stipple.svg b/apps/www/public/images/state-of-startups/pattern-stipple.svg
new file mode 100644
index 0000000000000..18949355d204f
--- /dev/null
+++ b/apps/www/public/images/state-of-startups/pattern-stipple.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/apps/www/public/images/state-of-startups/quote-avatars/jinal-j-120x120.jpg b/apps/www/public/images/state-of-startups/quote-avatars/jinal-j-120x120.jpg
new file mode 100644
index 0000000000000..cd44a06af99b1
Binary files /dev/null and b/apps/www/public/images/state-of-startups/quote-avatars/jinal-j-120x120.jpg differ
diff --git a/apps/www/public/images/state-of-startups/quote-avatars/kevinton-b-120x120.jpg b/apps/www/public/images/state-of-startups/quote-avatars/kevinton-b-120x120.jpg
new file mode 100644
index 0000000000000..386c3ffd14bc7
Binary files /dev/null and b/apps/www/public/images/state-of-startups/quote-avatars/kevinton-b-120x120.jpg differ
diff --git a/apps/www/public/images/state-of-startups/quote-avatars/richard-k-120x120.jpg b/apps/www/public/images/state-of-startups/quote-avatars/richard-k-120x120.jpg
new file mode 100644
index 0000000000000..3e7f4873a57ea
Binary files /dev/null and b/apps/www/public/images/state-of-startups/quote-avatars/richard-k-120x120.jpg differ
diff --git a/apps/www/public/images/state-of-startups/quote-avatars/robert-w-120x120.jpg b/apps/www/public/images/state-of-startups/quote-avatars/robert-w-120x120.jpg
new file mode 100644
index 0000000000000..9ae4abcbb962d
Binary files /dev/null and b/apps/www/public/images/state-of-startups/quote-avatars/robert-w-120x120.jpg differ
diff --git a/apps/www/public/images/state-of-startups/quote-avatars/waldemar-k-120x120.jpg b/apps/www/public/images/state-of-startups/quote-avatars/waldemar-k-120x120.jpg
new file mode 100644
index 0000000000000..fc82553139e56
Binary files /dev/null and b/apps/www/public/images/state-of-startups/quote-avatars/waldemar-k-120x120.jpg differ
diff --git a/apps/www/public/images/state-of-startups/2025/state-of-startups-og.png b/apps/www/public/images/state-of-startups/state-of-startups-og.png
similarity index 100%
rename from apps/www/public/images/state-of-startups/2025/state-of-startups-og.png
rename to apps/www/public/images/state-of-startups/state-of-startups-og.png
diff --git a/apps/www/public/images/twitter-profiles/09HouOSt_400x400.jpg b/apps/www/public/images/twitter-profiles/09HouOSt_400x400.jpg
deleted file mode 100644
index 9e4906f1d9ba3..0000000000000
Binary files a/apps/www/public/images/twitter-profiles/09HouOSt_400x400.jpg and /dev/null differ
diff --git a/apps/www/public/images/twitter-profiles/5KvPPRZz_400x400.jpg b/apps/www/public/images/twitter-profiles/5KvPPRZz_400x400.jpg
new file mode 100644
index 0000000000000..01d88b2ee2f0d
Binary files /dev/null and b/apps/www/public/images/twitter-profiles/5KvPPRZz_400x400.jpg differ
diff --git a/apps/www/public/images/twitter-profiles/6zCnwpvi_400x400.jpg b/apps/www/public/images/twitter-profiles/6zCnwpvi_400x400.jpg
deleted file mode 100644
index fe8ad8bf38a03..0000000000000
Binary files a/apps/www/public/images/twitter-profiles/6zCnwpvi_400x400.jpg and /dev/null differ
diff --git a/apps/www/public/images/twitter-profiles/89h9ROOs_400x400.jpg b/apps/www/public/images/twitter-profiles/89h9ROOs_400x400.jpg
new file mode 100644
index 0000000000000..d77ffdad44d16
Binary files /dev/null and b/apps/www/public/images/twitter-profiles/89h9ROOs_400x400.jpg differ
diff --git a/apps/www/public/images/twitter-profiles/EtC0mhne_400x400.jpg b/apps/www/public/images/twitter-profiles/EtC0mhne_400x400.jpg
new file mode 100644
index 0000000000000..e095c612f343b
Binary files /dev/null and b/apps/www/public/images/twitter-profiles/EtC0mhne_400x400.jpg differ
diff --git a/apps/www/public/images/twitter-profiles/FQsUZJMC_400x400.jpg b/apps/www/public/images/twitter-profiles/FQsUZJMC_400x400.jpg
new file mode 100644
index 0000000000000..f1e62671efac9
Binary files /dev/null and b/apps/www/public/images/twitter-profiles/FQsUZJMC_400x400.jpg differ
diff --git a/apps/www/public/images/twitter-profiles/GtrVV2dD_400x400.jpg b/apps/www/public/images/twitter-profiles/GtrVV2dD_400x400.jpg
new file mode 100644
index 0000000000000..e99772b64f1f2
Binary files /dev/null and b/apps/www/public/images/twitter-profiles/GtrVV2dD_400x400.jpg differ
diff --git a/apps/www/public/images/twitter-profiles/I5pY1PAA_400x400.jpg b/apps/www/public/images/twitter-profiles/I5pY1PAA_400x400.jpg
new file mode 100644
index 0000000000000..d248bc87626e2
Binary files /dev/null and b/apps/www/public/images/twitter-profiles/I5pY1PAA_400x400.jpg differ
diff --git a/apps/www/public/images/twitter-profiles/JwLEqyeo_400x400.jpg b/apps/www/public/images/twitter-profiles/JwLEqyeo_400x400.jpg
new file mode 100644
index 0000000000000..3831cd2eab7a2
Binary files /dev/null and b/apps/www/public/images/twitter-profiles/JwLEqyeo_400x400.jpg differ
diff --git a/apps/www/public/images/twitter-profiles/SvAyLaWV_400x400.jpg b/apps/www/public/images/twitter-profiles/SvAyLaWV_400x400.jpg
deleted file mode 100644
index 73072bcfdd635..0000000000000
Binary files a/apps/www/public/images/twitter-profiles/SvAyLaWV_400x400.jpg and /dev/null differ
diff --git a/apps/www/public/images/twitter-profiles/T42R9GFf_400x400.jpg b/apps/www/public/images/twitter-profiles/T42R9GFf_400x400.jpg
new file mode 100644
index 0000000000000..16a5041fa0275
Binary files /dev/null and b/apps/www/public/images/twitter-profiles/T42R9GFf_400x400.jpg differ
diff --git a/apps/www/public/images/twitter-profiles/XE8Oyngj_400x400.jpg b/apps/www/public/images/twitter-profiles/XE8Oyngj_400x400.jpg
new file mode 100644
index 0000000000000..ddb4e3e76fd99
Binary files /dev/null and b/apps/www/public/images/twitter-profiles/XE8Oyngj_400x400.jpg differ
diff --git a/apps/www/public/images/twitter-profiles/ZmOmQeTl_400x400.jpg b/apps/www/public/images/twitter-profiles/ZmOmQeTl_400x400.jpg
new file mode 100644
index 0000000000000..38afc1d167fb8
Binary files /dev/null and b/apps/www/public/images/twitter-profiles/ZmOmQeTl_400x400.jpg differ
diff --git a/apps/www/public/images/twitter-profiles/nbmmwDll_400x400.jpg b/apps/www/public/images/twitter-profiles/nbmmwDll_400x400.jpg
new file mode 100644
index 0000000000000..9c9df1b2842df
Binary files /dev/null and b/apps/www/public/images/twitter-profiles/nbmmwDll_400x400.jpg differ
diff --git a/apps/www/public/images/twitter-profiles/rLgwUZSB_400x400.jpg b/apps/www/public/images/twitter-profiles/rLgwUZSB_400x400.jpg
deleted file mode 100644
index 7623e0f2bcbd3..0000000000000
Binary files a/apps/www/public/images/twitter-profiles/rLgwUZSB_400x400.jpg and /dev/null differ
diff --git a/apps/www/public/images/twitter-profiles/u9qcTMAS_400x400.jpg b/apps/www/public/images/twitter-profiles/u9qcTMAS_400x400.jpg
new file mode 100644
index 0000000000000..1135c4f2002c3
Binary files /dev/null and b/apps/www/public/images/twitter-profiles/u9qcTMAS_400x400.jpg differ
diff --git a/apps/www/public/images/twitter-profiles/ukFtCkww_400x400.jpg b/apps/www/public/images/twitter-profiles/ukFtCkww_400x400.jpg
new file mode 100644
index 0000000000000..d3bc7d5faeae7
Binary files /dev/null and b/apps/www/public/images/twitter-profiles/ukFtCkww_400x400.jpg differ
diff --git a/packages/shared-data/tweets.ts b/packages/shared-data/tweets.ts
index 26fd45c97be34..fddb5b92e4232 100644
--- a/packages/shared-data/tweets.ts
+++ b/packages/shared-data/tweets.ts
@@ -1,4 +1,82 @@
const tweets = [
+ {
+ text: "Really impressed with @supabase's Assistant.\n\nIt has helped me troubleshoot and solve complex CORS Configuration issues on Pinger.",
+ url: 'https://x.com/TyronBache/status/1924425289959928039',
+ handle: 'TyronBache',
+ img_url: '/images/twitter-profiles/89h9ROOs_400x400.jpg',
+ },
+ {
+ text: 'I’ve always used Supabase just as a database.\n\nYesterday, I helped debug a founder’s vibe-coding project built with React + React Router — no backend server.\nThe “backend” was entirely Supabase Edge Functions as the API.\nFirst time using Supabase this way.\nImpressive.',
+ url: 'https://x.com/MinimEditor/status/1954422981708722372',
+ handle: 'MinimEditor',
+ img_url: '/images/twitter-profiles/5KvPPRZz_400x400.jpg',
+ },
+ {
+ text: 'Love @supabase custom domains\n\nmakes the auth so much better',
+ url: 'https://x.com/orlandopedro_/status/1958618806143578336',
+ handle: 'orlandopedro_',
+ img_url: '/images/twitter-profiles/JwLEqyeo_400x400.jpg',
+ },
+ {
+ text: 'Loving #Supabase MCP. Claude Code would not only plan what data we should save but also figure out a migration script by checking what the schema looks like on Supabase via MCP.',
+ url: 'https://x.com/sdusteric/status/1957703488470921550',
+ handle: 'sdusteric',
+ img_url: '/images/twitter-profiles/FQsUZJMC_400x400.jpg',
+ },
+ {
+ text: "I love @supabase's built-in Advisors. The security and performance linters improve everything and boost my confidence in what I'm building!",
+ url: 'https://x.com/SteinlageScott/status/1958603243401183701',
+ handle: 'SteinlageScott',
+ img_url: '/images/twitter-profiles/nbmmwDll_400x400.jpg',
+ },
+ {
+ text: "Working with @supabase has been one of the best dev experiences I've had lately.\n\nIncredibly easy to set up, great documentation, and so many fewer hoops to jump through than the competition.\n\nI definitely plan to use it on any and all future projects.",
+ url: 'https://x.com/BowTiedQilin/status/1497602628410388480',
+ handle: 'BowTiedQilin',
+ img_url: '/images/twitter-profiles/ZmOmQeTl_400x400.jpg',
+ },
+ {
+ text: 'Love supabse edge functions. Cursor+Supabase+MCP+Docker desktop is all I need',
+ url: 'https://x.com/adm_lawson/status/1958216298309066887',
+ handle: 'adm_lawson',
+ img_url: '/images/twitter-profiles/I5pY1PAA_400x400.jpg',
+ },
+ {
+ text: 'First time running @supabase in local. It just works. Very good DX imo.',
+ url: 'https://x.com/gokul_i/status/1958880167889133811',
+ handle: 'gokul_i',
+ img_url: '/images/twitter-profiles/EtC0mhne_400x400.jpg',
+ },
+ {
+ text: 'Run supabase locally and just wow in silence! I am impressed! This is the kind of tooling I would want for my team.',
+ url: 'https://x.com/dadooos_/status/1947924753618243663',
+ handle: 'dadooos_',
+ img_url: '/images/twitter-profiles/T42R9GFf_400x400.jpg',
+ },
+ {
+ text: "After a week of diving deep into Supabase for my new SaaS project, I'm really impressed with its Auth and RLS features. It makes security much simpler for solo founders. #buildinpublic #SaaS",
+ url: 'https://x.com/Rodrigo66799141/status/1959246083957100851',
+ handle: 'Rodrigo66799141',
+ img_url: '/images/twitter-profiles/ukFtCkww_400x400.jpg',
+ },
+ {
+ text: 'Lately been using Supabase over AWS/ GCP for products to save on costs and rapid builds(Vibe Code) that do not need all the Infra and the hefty costs that come with AWS/ GCP out the door. Great solution overall. Love the new Feature stack thats implemented',
+ url: 'https://x.com/xthemadgeniusx/status/1960049950110384250',
+ handle: 'xthemadgeniusx',
+ img_url: '/images/twitter-profiles/XE8Oyngj_400x400.jpg',
+ },
+ {
+ text: 'I love everything about Supabase.',
+ url: 'https://x.com/pontusab/status/1958603243401183701',
+ handle: 'pontusab',
+ img_url: '/images/twitter-profiles/JwLEqyeo_400x400.jpg',
+ },
+ {
+ text: 'Love how Supabase makes full stack features this easy. Using it with Next.js and loving the experience!',
+ url: 'https://x.com/viratt_mankali/status/1963290133421240591',
+ handle: 'viratt_mankali',
+ img_url: '/images/twitter-profiles/GtrVV2dD_400x400.jpg',
+ },
{
text: "Working with @supabase has been one of the best dev experiences I've had lately. Incredibly easy to set up, great documentation, and so many fewer hoops to jump through than the competition. I definitely plan to use it on any and all future projects.",
url: 'https://twitter.com/thatguy_tex/status/1497602628410388480',
@@ -24,7 +102,7 @@ const tweets = [
img_url: '/images/twitter-profiles/9k_ZB9OO_400x400.jpg',
},
{
- text: 'And thanks to @supabase, I was able to go from idea to launched feature in a matter of hours. Absolutely amazing!',
+ text: 'And thanks to @supabase, I was able to go from idea to launched feature in a matter of hours.\n\nAbsolutely amazing!',
url: 'https://twitter.com/BraydonCoyer/status/1511071369731137537',
handle: 'BraydonCoyer',
img_url: '/images/twitter-profiles/8YxkpW8f_400x400.jpg',
@@ -209,12 +287,6 @@ const tweets = [
handle: '_strawbird',
img_url: '/images/twitter-profiles/iMBvvQdn_400x400.jpg',
},
- {
- text: "Did a website with @supabase last week with no prior experience with it. Up and running in 20 minutes. It's awesome to use. Thumbs up",
- url: 'https://twitter.com/michael_webdev/status/1352885366928404481?s=20',
- handle: 'michael_webdev',
- img_url: '/images/twitter-profiles/SvAyLaWV_400x400.jpg',
- },
{
text: 'I just learned about @supabase and im in love 😍 Supabase is an open source Firebase alternative! EarListen (& react) to database changes 💁 Manage users & permissions 🔧 Simple UI for database interaction',
url: 'https://twitter.com/0xBanana/status/1373677301905362948',
diff --git a/packages/ui-patterns/src/TimestampInfo/index.tsx b/packages/ui-patterns/src/TimestampInfo/index.tsx
index f259abad456ee..7fa5d6e3b2f73 100644
--- a/packages/ui-patterns/src/TimestampInfo/index.tsx
+++ b/packages/ui-patterns/src/TimestampInfo/index.tsx
@@ -53,12 +53,14 @@ export const TimestampInfo = ({
displayAs = 'local',
format = 'DD MMM HH:mm:ss',
labelFormat = 'DD MMM HH:mm:ss',
+ label,
}: {
className?: string
utcTimestamp: string | number
displayAs?: 'local' | 'utc'
format?: string
labelFormat?: string
+ label?: string
}) => {
const local = timestampLocalFormatter({ utcTimestamp, format })
const utc = timestampUtcFormatter({ utcTimestamp, format })
@@ -139,9 +141,11 @@ export const TimestampInfo = ({
className={`text-xs ${className} border-b border-transparent hover:border-dashed hover:border-foreground-light`}
>
- {displayAs === 'local'
- ? timestampLocalFormatter({ utcTimestamp, format: labelFormat })
- : timestampUtcFormatter({ utcTimestamp, format: labelFormat })}
+ {label
+ ? label
+ : displayAs === 'local'
+ ? timestampLocalFormatter({ utcTimestamp, format: labelFormat })
+ : timestampUtcFormatter({ utcTimestamp, format: labelFormat })}
diff --git a/packages/ui-patterns/src/TweetCard/index.tsx b/packages/ui-patterns/src/TweetCard/index.tsx
index 1fbd7f36ab13c..bcaa26c49a1f4 100644
--- a/packages/ui-patterns/src/TweetCard/index.tsx
+++ b/packages/ui-patterns/src/TweetCard/index.tsx
@@ -42,7 +42,7 @@ export function TweetCard(props: TweetCard) {
- "{props.quote}"
+ {props.quote}
)
}
diff --git a/packages/ui/src/components/SimpleCodeBlock/SimpleCodeBlock.tsx b/packages/ui/src/components/SimpleCodeBlock/SimpleCodeBlock.tsx
index 6ec54244c1708..20e15661f36dd 100644
--- a/packages/ui/src/components/SimpleCodeBlock/SimpleCodeBlock.tsx
+++ b/packages/ui/src/components/SimpleCodeBlock/SimpleCodeBlock.tsx
@@ -9,9 +9,9 @@
import { useTheme } from 'next-themes'
import { Highlight, Language, Prism, themes } from 'prism-react-renderer'
import { PropsWithChildren, useEffect, useRef, useState } from 'react'
-import { Button } from './../Button'
-import { cn } from './../../lib/utils/cn'
import { copyToClipboard } from '../../lib/utils'
+import { cn } from './../../lib/utils/cn'
+import { Button } from './../Button'
import { dart } from './prism'
dart(Prism)
@@ -65,7 +65,7 @@ export const SimpleCodeBlock = ({
{tokens.map((line, i) => {
- const lineProps = getLineProps({ line, key: i })
+ const { key: _key, ...lineProps } = getLineProps({ line, key: i })
if (highlightLines.includes(i + 1)) {
lineProps.className = `${lineProps.className} docusaurus-highlight-code-line`
@@ -73,9 +73,10 @@ export const SimpleCodeBlock = ({
return (
- {line.map((token, key) => (
-
- ))}
+ {line.map((token, key) => {
+ const { key: _key, ...tokenProps } = getTokenProps({ token, key })
+ return
+ })}
)
})}
diff --git a/turbo.json b/turbo.json
index d67bdaf8ca63f..abec773d5910f 100644
--- a/turbo.json
+++ b/turbo.json
@@ -131,6 +131,8 @@
"HCAPTCHA_SECRET_KEY",
"NODE_ENV",
"NEXT_PUBLIC_SENTRY_DSN",
+ "NEXT_PUBLIC_SURVEY_SUPABASE_URL",
+ "NEXT_PUBLIC_SURVEY_SUPABASE_ANON_KEY",
// These envs are used in the packages
"NEXT_PUBLIC_STORAGE_KEY",
"NEXT_PUBLIC_AUTH_DEBUG_KEY",