Files
supabase/apps/studio/pages/redeem.tsx
Joshen LimandKevin Grüneberg 5b8ca1bb66 Chore/fix redeem code UI (#42615)
## Context

Main fix was to address the size of the `OrganizationCard` on the redeem
code page - was taking up the height of the flex div when the number of
cards is <= 3 (Main fix is `max-h-min` in `OrganizationCard`)

### Before
<img width="1270" height="411" alt="image"
src="https://github.com/user-attachments/assets/73228b62-f9db-43c8-813b-cb04c9f6cb2b"
/>

### After
<img width="1255" height="420" alt="image"
src="https://github.com/user-attachments/assets/a1d82753-daba-4c3d-a64d-3bb913ded161"
/>

## Other changes
- Small improvement to perceived loading speed by only rendering the
cards as loaders, text content on the left should render immediately on
page load
- Loading state to consider if the feature flag has been loaded as well
to prevent flicker of UI
- Hide the "Go to org" button when redeemed on the org page already


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

* **Bug Fixes**
* Improved loading/gating so the redemption UI waits for feature flags
and profile data before showing content.
* Disabled submit when the redemption form is invalid to prevent empty
submissions; submit button respects form validity, permissions, and
loading state.

* **UI**
* Conditionally shows upgrade and "Go to organization" options based on
organization plan and current path.
  * Adjusted card layout to better constrain content height.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Kevin Grüneberg <k.grueneberg1994@gmail.com>
2026-02-10 10:36:30 +08:00

115 lines
3.7 KiB
TypeScript

import { FeatureFlagContext, useFlag } from 'common'
import Link from 'next/link'
import { useContext, useState } from 'react'
import { Button } from 'ui'
import { Admonition, ShimmeringLoader } from 'ui-patterns'
import { CreditCodeRedemption } from '@/components/interfaces/Organization/BillingSettings/CreditCodeRedemption'
import { OrganizationCard } from '@/components/interfaces/Organization/OrganizationCard'
import RedeemCreditsLayout from '@/components/layouts/RedeemCreditsLayout'
import {
ScaffoldContainer,
ScaffoldDivider,
ScaffoldHeader,
ScaffoldTitle,
} from '@/components/layouts/Scaffold'
import AlertError from '@/components/ui/AlertError'
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
import { useProfile } from '@/lib/profile'
import type { NextPageWithLayout } from '@/types'
const RedeemCreditsContent = () => {
const redeemCodeEnabled = useFlag('redeemCodeEnabled')
const { isLoading: isLoadingProfile } = useProfile()
const { hasLoaded } = useContext(FeatureFlagContext)
const [selectedOrg, setSelectedOrg] = useState<string | null>(null)
const {
data: organizations,
error: errorOrganizations,
isLoading: isLoadingOrganizations,
isError: isErrorOrganizations,
} = useOrganizationsQuery()
if (isErrorOrganizations) {
return (
<AlertError
className="mt-4"
error={errorOrganizations}
subject="Failed to retrieve organizations"
/>
)
}
return (
<div className="grid md:grid-cols-2 pt-10 gap-8">
<div className="flex flex-col gap-y-8">
<div className="flex flex-col gap-y-1">
<p>To redeem your credits, select one of your organizations.</p>
<p className="text-sm text-foreground-light text-balance">
The credits will be applied to that organization only and cannot be transferred or
shared between organizations.
</p>
</div>
<div>
<p>Want to start fresh?</p>
<p className="mt-1 text-sm text-foreground-light text-balance">
Create a new organization first. You will have to revisit this link after creating the
organization to redeem the code.
</p>
<Button asChild className="mt-4 w-min" size="tiny" type="primary">
<Link href="/new">Create organization</Link>
</Button>
</div>
</div>
<div className="flex flex-col gap-y-2">
{/* [Joshen] Checking for profile as well as organizations query internally waits for profile to be loaded */}
{isLoadingProfile || isLoadingOrganizations || !hasLoaded ? (
<>
<ShimmeringLoader className="w-full h-[70px]" />
<ShimmeringLoader className="w-full h-[70px]" />
</>
) : redeemCodeEnabled ? (
organizations?.map((org) => (
<OrganizationCard
key={org.id}
isLink={false}
organization={org}
onClick={() => setSelectedOrg(org.slug)}
/>
))
) : (
<Admonition type="note" title="Code redemption coming soon" />
)}
</div>
{selectedOrg && (
<CreditCodeRedemption
modalVisible
slug={selectedOrg}
onClose={() => setSelectedOrg(null)}
/>
)}
</div>
)
}
const RedeemCreditsPage: NextPageWithLayout = () => {
return (
<ScaffoldContainer>
<ScaffoldHeader>
<ScaffoldTitle>Code Redemption</ScaffoldTitle>
</ScaffoldHeader>
<ScaffoldDivider />
<RedeemCreditsContent />
</ScaffoldContainer>
)
}
RedeemCreditsPage.getLayout = (page) => <RedeemCreditsLayout>{page}</RedeemCreditsLayout>
export default RedeemCreditsPage