Files
Ali Waseem 9c716a6a48 fix(studio): make paused project dashboard view responsive on mobile (#45068)
Closes
[FE-3040](https://linear.app/supabase/issue/FE-3040/fix-paused-project-dashboard-view-on-mobile)

## Summary

The paused project dashboard card was locked to a fixed `w-[40rem]`
(640px) width, causing horizontal overflow on mobile viewports. The
Resume project button was pushed off-screen, which drove users into
Project Settings → General, where the only visible action (Restart) is
disabled for paused projects.

## Changes

- `ProjectPausedState` card: `w-[40rem]` → `w-full max-w-[40rem]`
- `CardFooter`: added `flex-wrap` so Resume +
Upgrade-to-Pro/View-settings buttons stay visible on narrow screens
- `PauseDisabledState` export-data footer: stacks on mobile, row layout
on `sm:`+
- Parent wrappers (`ProjectLayout/index.tsx`, `Home.tsx`): added `px-4`
so the card has breathing room from viewport edges

## Follow-up (not in this PR)

The Linear issue also suggests adding a Resume action (or differentiated
messaging) to Project Settings → General's "Project availability"
section, so users who land there on a paused project have a clear path
forward. Happy to tackle that separately.

## Test plan

- [x] Paused Free-plan project on mobile viewport (≤640px): Resume +
Upgrade buttons visible, text wraps within the card
- [x] Paused Pro-plan project on mobile viewport: Resume + View settings
buttons visible
- [x] Non-restorable (>90 days paused) project on mobile: Recovery
options list readable, "Download backups" dropdown visible and stacks
below description
- [x] Desktop (≥640px): card still renders at 40rem max width, layout
unchanged
- [x] Verified via `/project/[ref]` and `/project/[ref]/home` entry
points

## Demo
<img width="443" height="832" alt="image"
src="https://github.com/user-attachments/assets/d5b7713e-c0b5-44e8-82fe-98c3308d5e8b"
/>

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Style**
* Added horizontal padding to the paused project state interface and
wrapper.
* Made the export data section layout responsive, stacking vertically on
mobile and horizontally on larger screens.
* Updated the paused state card container styling to use responsive
width constraints instead of fixed dimensions.
  * Adjusted footer element spacing, gap sizing, and wrapping behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-20 13:45:22 -06:00

287 lines
13 KiB
TypeScript

import { useParams } from 'common'
import dayjs from 'dayjs'
import Link from 'next/link'
import { useEffect, useRef } from 'react'
import {
Badge,
cn,
Tabs_Shadcn_,
TabsContent_Shadcn_,
TabsList_Shadcn_,
TabsTrigger_Shadcn_,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { AdvisorWidget } from '@/components/interfaces/Home/AdvisorWidget'
import { ClientLibrary } from '@/components/interfaces/Home/ClientLibrary'
import { ExampleProject } from '@/components/interfaces/Home/ExampleProject'
import { 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 { useProjectDetailQuery } from '@/data/projects/project-detail-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, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { DOCS_URL, IS_PLATFORM, PROJECT_STATUS } from '@/lib/constants'
import { useAppStateSnapshot } from '@/state/app-state'
export const Home = () => {
const { data: project } = useSelectedProjectQuery()
const { data: organization } = useSelectedOrganizationQuery()
const { data: parentProject } = useProjectDetailQuery({ ref: project?.parent_project_ref })
const isOrioleDb = useIsOrioleDb()
const snap = useAppStateSnapshot()
const { ref, enableBranching } = useParams()
const { projectHomepageExampleProjects, projectHomepageClientLibraries: clientLibraries } =
useCustomContent(['project_homepage:example_projects', 'project_homepage:client_libraries'])
const {
projectHomepageShowInstanceSize: showInstanceSize,
projectHomepageShowExamples: showExamples,
} = useIsFeatureEnabled(['project_homepage:show_instance_size', 'project_homepage:show_examples'])
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, isPending: isLoadingTables } = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: 'public',
})
const { data: functionsData, isPending: isLoadingFunctions } = useEdgeFunctionsQuery({
projectRef: project?.ref,
})
const { data: replicasData, isPending: 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)
if (isPaused) {
return (
<div className="w-full h-full flex items-center justify-center px-4">
<ProjectPausedState />
</div>
)
}
return (
<div className="w-full px-4">
<div className={cn('py-16 ', !isPaused && 'border-b border-muted ')}>
<div className="mx-auto max-w-7xl flex flex-col gap-y-4">
<div className="flex flex-col md:flex-row md:items-center gap-6 justify-between w-full">
<div className="flex flex-col md:flex-row md:items-end gap-3 w-full">
<div>
{!isMainBranch && (
<Link
href={`/project/${parentProject?.ref}`}
className="text-sm text-foreground-light"
>
{parentProject?.name}
</Link>
)}
<h1 className="text-3xl">{projectName}</h1>
</div>
<div className="flex items-center gap-x-2 mb-1">
{isOrioleDb && (
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="warning">OrioleDB</Badge>
</TooltipTrigger>
<TooltipContent side="bottom" align="start" className="max-w-80 text-center">
This project is using Postgres with OrioleDB which is currently in preview and
not suitable for production workloads. View our{' '}
<InlineLink href={`${DOCS_URL}/guides/database/orioledb`}>
documentation
</InlineLink>{' '}
for all limitations.
</TooltipContent>
</Tooltip>
)}
{showInstanceSize && (
<ComputeBadgeWrapper
projectRef={project?.ref}
slug={organization?.slug}
cloudProvider={project?.cloud_provider}
computeSize={project?.infra_compute_size}
/>
)}
</div>
</div>
<div className="flex items-center">
{project?.status === PROJECT_STATUS.ACTIVE_HEALTHY && (
<div className="flex items-center gap-x-6">
<div className="flex flex-col gap-y-1">
<Link
href={`/project/${ref}/editor`}
className="transition text-foreground-light hover:text-foreground text-sm"
>
Tables
</Link>
{isLoadingTables ? (
<ShimmeringLoader className="w-full h-[32px] w-6 p-0" />
) : (
<p className="text-2xl tabular-nums">{tablesCount}</p>
)}
</div>
<div className="flex flex-col gap-y-1">
<Link
href={`/project/${ref}/functions`}
className="transition text-foreground-light hover:text-foreground text-sm"
>
Functions
</Link>
{isLoadingFunctions ? (
<ShimmeringLoader className="w-full h-[32px] w-6 p-0" />
) : (
<p className="text-2xl tabular-nums">{functionsCount}</p>
)}
</div>
{IS_PLATFORM && (
<div className="flex flex-col gap-y-1">
<Link
href={`/project/${ref}/settings/infrastructure`}
className="transition text-foreground-light hover:text-foreground text-sm"
>
Replicas
</Link>
{isLoadingReplicas ? (
<ShimmeringLoader className="w-full h-[32px] w-6 p-0" />
) : (
<p className="text-2xl tabular-nums">{replicasCount}</p>
)}
</div>
)}
</div>
)}
{IS_PLATFORM && project?.status === PROJECT_STATUS.ACTIVE_HEALTHY && (
<div className="ml-6 border-l flex items-center w-[145px] justify-end">
<ServiceStatus />
</div>
)}
</div>
</div>
<ProjectUpgradeFailedBanner />
</div>
</div>
<>
<div className="py-16 border-b border-muted">
<div className="mx-auto max-w-7xl space-y-16 @container">
{IS_PLATFORM && project?.status !== PROJECT_STATUS.INACTIVE && (
<>{isNewProject ? <NewProjectPanel /> : <ProjectUsageSection />}</>
)}
{!isNewProject && project?.status !== PROJECT_STATUS.INACTIVE && <AdvisorWidget />}
</div>
</div>
<div className="bg-surface-100/5 py-16">
<div className="mx-auto max-w-7xl space-y-16 @container">
{project?.status !== PROJECT_STATUS.INACTIVE && (
<>
<div className="space-y-8">
<h2 className="text-lg">Client libraries</h2>
<div className="grid grid-cols-2 gap-x-8 gap-y-8 md:gap-12 mb-12 md:grid-cols-3">
{clientLibraries!.map((library) => (
// [Alaister]: Looks like the useCustomContent has wonky types. I'll look at a fix later.
<ClientLibrary key={(library as any).language} {...(library as any)} />
))}
</div>
</div>
{showExamples && (
<div className="flex flex-col gap-y-8">
<h4 className="text-lg">Example projects</h4>
{!!projectHomepageExampleProjects ? (
<div className="grid gap-2 md:gap-8 md:grid-cols-2 lg:grid-cols-3">
{/* [Alaister]: Looks like the useCustomContent has wonky types. I'll look at a fix later. */}
{(projectHomepageExampleProjects as any)
.sort((a: any, b: any) => a.title.localeCompare(b.title))
.map((project: any) => (
<ExampleProject key={project.url} {...project} />
))}
</div>
) : (
<div className="flex justify-center">
<Tabs_Shadcn_ defaultValue="app" className="w-full">
<TabsList_Shadcn_ className="flex gap-4 mb-8">
<TabsTrigger_Shadcn_ value="app">App Frameworks</TabsTrigger_Shadcn_>
<TabsTrigger_Shadcn_ value="mobile">
Mobile Frameworks
</TabsTrigger_Shadcn_>
</TabsList_Shadcn_>
<TabsContent_Shadcn_ value="app">
<div className="grid gap-2 md:gap-8 md:grid-cols-2 lg:grid-cols-3">
{EXAMPLE_PROJECTS.filter((project) => project.type === 'app')
.sort((a, b) => a.title.localeCompare(b.title))
.map((project) => (
<ExampleProject key={project.url} {...project} />
))}
</div>
</TabsContent_Shadcn_>
<TabsContent_Shadcn_ value="mobile">
<div className="grid gap-2 md:gap-8 md:grid-cols-2 lg:grid-cols-3">
{EXAMPLE_PROJECTS.filter((project) => project.type === 'mobile')
.sort((a, b) => a.title.localeCompare(b.title))
.map((project) => (
<ExampleProject key={project.url} {...project} />
))}
</div>
</TabsContent_Shadcn_>
</Tabs_Shadcn_>
</div>
)}
</div>
)}
</>
)}
</div>
</div>
</>
</div>
)
}