From 6448849f77c61839679e1627bdfc7c99ffcfe3c0 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Mon, 18 May 2026 19:36:44 +0000 Subject: [PATCH] fix(web): stabilize home recents and improve org plan switcher (#967) - Keep home Recently saved and suggestions in a stable two-column layout - Show skeleton while recents load and empty state when none exist - Cap org switcher dropdown height with scroll for long org lists - Show plan tier badges on all orgs, sorted by plan rank --- apps/web/components/dashboard-view.tsx | 93 +++++++++++------- apps/web/components/settings/account.tsx | 118 +++++++++++++++++++---- apps/web/hooks/use-account-settings.ts | 1 + apps/web/hooks/use-token-usage.ts | 24 +++++ 4 files changed, 181 insertions(+), 55 deletions(-) diff --git a/apps/web/components/dashboard-view.tsx b/apps/web/components/dashboard-view.tsx index 411f0b89..38f13ee3 100644 --- a/apps/web/components/dashboard-view.tsx +++ b/apps/web/components/dashboard-view.tsx @@ -655,7 +655,7 @@ export function DashboardView({ const { user } = useAuth() const { effectiveContainerTags } = useProject() const _router = useRouter() - const { data: recentsData } = useQuery({ + const { data: recentsData, isPending: isRecentsLoading } = useQuery({ queryKey: ["dashboard-recents", effectiveContainerTags], queryFn: async (): Promise => { const response = await $fetch("@post/documents/documents", { @@ -894,25 +894,43 @@ export function DashboardView({ transition={{ ...fadeUp.transition, delay: 0.15 }} className="space-y-2" > - {recents.length > 0 ? ( - <> - {/* Shared header row — both labels aligned */} -
-
-

- Recently saved -

-
-
-

- Suggested for you -

-
-
+
+
+

+ Recently saved +

+
+
+

+ Suggested for you +

+
+
- {/* Content row */} -
-
+ + {(isRecentsLoading || recents.length === 0) && ( +

Suggested for you

@@ -967,7 +990,7 @@ export function DashboardView({ onOpenIntegrations={onOpenIntegrations} />
- + )} diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx index 676e251a..978d8fc3 100644 --- a/apps/web/components/settings/account.tsx +++ b/apps/web/components/settings/account.tsx @@ -8,7 +8,13 @@ import { useDeleteUserAccount, } from "@/hooks/use-account-settings" import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar" -import { useTokenUsage } from "@/hooks/use-token-usage" +import { + normalizePlanType, + PLAN_DISPLAY_NAMES, + PLAN_RANK, + useTokenUsage, + type PlanType, +} from "@/hooks/use-token-usage" import { Dialog, DialogContent, @@ -187,6 +193,43 @@ function formatOrgRole(role: string): string { : "Member" } +/** Matches ACTIVE / RECOMMENDED pills in billing settings. */ +const orgPlanBadgeBase = cn( + dmSans125ClassName(), + "inline-flex h-[18px] min-w-[42px] shrink-0 items-center justify-center rounded-[3px] px-1.5 text-[10px] uppercase", +) + +const ORG_PLAN_BADGE_STYLES: Record = { + free: "bg-[#2E353D] font-mono font-medium tracking-[0.12em] text-[#A3A3A3]", + pro: "bg-[#4BA0FA] font-bold tracking-[0.36px] text-[#00171A]", + scale: "bg-[#0054AD] font-bold tracking-[0.36px] text-[#FAFAFA]", + enterprise: "bg-[#FAFAFA] font-bold tracking-[0.36px] text-[#0D121A]", +} + +function OrgPlanBadge({ plan }: { plan: PlanType }) { + return ( + + {PLAN_DISPLAY_NAMES[plan]} + + ) +} + +function resolveOrgPlan( + orgId: string, + organization: { metadata?: unknown }, + isCurrent: boolean, + currentPlan: PlanType, + membershipPlanByOrgId: Map, +): PlanType { + if (isCurrent) return currentPlan + + const fromMembership = membershipPlanByOrgId.get(orgId) + if (fromMembership) return fromMembership + + const metadata = organization.metadata as Record | null + return normalizePlanType(metadata?.plan ?? metadata?.subscriptionPlan) +} + export default function Account() { const { user, @@ -261,12 +304,40 @@ export default function Account() { maximumFractionDigits: 2, }) - const planDisplayNames: Record = { - free: "Free", - pro: "Pro", - scale: "Scale", - enterprise: "Enterprise", - } + const planDisplayNames = PLAN_DISPLAY_NAMES + + const membershipPlanByOrgId = useMemo(() => { + const map = new Map() + for (const membership of memberships ?? []) { + if (membership.plan) { + map.set(membership.orgId, normalizePlanType(membership.plan)) + } + } + return map + }, [memberships]) + + const sortedOrgsForMenu = useMemo(() => { + if (!allOrgs?.length) return [] + return [...allOrgs].sort((a, b) => { + const planA = resolveOrgPlan( + a.id, + a, + a.id === org?.id, + currentPlan, + membershipPlanByOrgId, + ) + const planB = resolveOrgPlan( + b.id, + b, + b.id === org?.id, + currentPlan, + membershipPlanByOrgId, + ) + const rankDiff = PLAN_RANK[planB] - PLAN_RANK[planA] + if (rankDiff !== 0) return rankDiff + return a.name.localeCompare(b.name) + }) + }, [allOrgs, org?.id, currentPlan, membershipPlanByOrgId]) // Handlers const handleUpgrade = async () => { @@ -430,11 +501,18 @@ export default function Account() { {canSwitchOrg && ( - {allOrgs?.map((organization) => { + {sortedOrgsForMenu.map((organization) => { const isCurrent = organization.id === org?.id const isSwitching = switchingOrgId === organization.id + const plan = resolveOrgPlan( + organization.id, + organization, + isCurrent, + currentPlan, + membershipPlanByOrgId, + ) return ( ) })} diff --git a/apps/web/hooks/use-account-settings.ts b/apps/web/hooks/use-account-settings.ts index ca2fae62..d073d36f 100644 --- a/apps/web/hooks/use-account-settings.ts +++ b/apps/web/hooks/use-account-settings.ts @@ -10,6 +10,7 @@ export type AccountMembership = { slug: string role: string memberCount: number + plan?: string } export function useAccountMemberships() { diff --git a/apps/web/hooks/use-token-usage.ts b/apps/web/hooks/use-token-usage.ts index c903d7e7..898d89ac 100644 --- a/apps/web/hooks/use-token-usage.ts +++ b/apps/web/hooks/use-token-usage.ts @@ -4,6 +4,30 @@ import { calculateUsagePercent, getDaysRemaining } from "@/lib/billing-utils" export type PlanType = "free" | "pro" | "scale" | "enterprise" +export const PLAN_DISPLAY_NAMES: Record = { + free: "Free", + pro: "Pro", + scale: "Scale", + enterprise: "Enterprise", +} + +/** Higher rank sorts first in org lists (enterprise at top). */ +export const PLAN_RANK: Record = { + free: 0, + pro: 1, + scale: 2, + enterprise: 3, +} + +export function normalizePlanType(raw: unknown): PlanType { + if (typeof raw !== "string" || !raw.trim()) return "free" + const normalized = raw.toLowerCase().replace(/^api_/, "") + if (normalized === "enterprise") return "enterprise" + if (normalized === "scale") return "scale" + if (normalized === "pro") return "pro" + return "free" +} + const TOKEN_METER_IDS = [ "sm_tokens_text", "sm_tokens_rich",