From cd6b125eee9d1fbc0828daa801e392a2b6f26918 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sat, 20 Dec 2025 14:18:05 +1100 Subject: [PATCH 1/2] Improve pricing page --- .../components/marketing/pricing-section.tsx | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/apps/web/components/marketing/pricing-section.tsx b/apps/web/components/marketing/pricing-section.tsx index d08a3ea..fdef8c4 100644 --- a/apps/web/components/marketing/pricing-section.tsx +++ b/apps/web/components/marketing/pricing-section.tsx @@ -4,18 +4,34 @@ import Link from "next/link"; import { ROUTES } from "../../data/routes.data"; import background from "../../public/images/hero/pricing.jpg"; -export default function PricingSection({ unit_amount = 500, addons = [] }) { +interface Addon { + price: number; + name: string; +} + +interface PricingSectionProps { + unit_amount?: number; + addons?: Addon[]; +} + +export default function PricingSection({ + unit_amount = 200, + addons = [], +}: PricingSectionProps) { + const priceInDollars = unit_amount / 100; const features = [ - "Custom domain + SSL", "Email notifications (add-on)", - "Post Scheduling", - "Audience Analytics", - "SEO Friendly", - "Embeddable Widget", - "Zapier Integration", - "White labeling", - "AI Assistant", - "Email & Slack Support", + "Public roadmap with community voting", + "Post scheduling, reactions & pinned posts", + "Team collaboration & member invites", + "Custom domain + SSL", + "GitHub Changelog Agent", + "Markdown editor with image uploads", + "Audience analytics", + "JSON API & RSS feed", + "React SDK & embeddable widget", + "Zapier & GitHub integration", + "SEO friendly", ]; return ( @@ -36,7 +52,7 @@ export default function PricingSection({ unit_amount = 500, addons = [] }) { Simple Pricing

- Everything you need for just ${Number(unit_amount) / 100 || "5"}{" "} + Everything you need for just ${priceInDollars}

@@ -77,11 +93,11 @@ export default function PricingSection({ unit_amount = 500, addons = [] }) {

-
+

- ${Number(unit_amount / 100)} + ${priceInDollars} /page /mo @@ -116,6 +132,9 @@ export default function PricingSection({ unit_amount = 500, addons = [] }) { > Start free trial +

+ 14-days free trial +

From e8cb1d947863a6fb1483cd8d484165035099c0c6 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sat, 20 Dec 2025 14:42:14 +1100 Subject: [PATCH 2/2] Display storage usage in billing --- apps/web/pages/account/billing.tsx | 115 ++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/apps/web/pages/account/billing.tsx b/apps/web/pages/account/billing.tsx index a16a5e0..1a3b050 100644 --- a/apps/web/pages/account/billing.tsx +++ b/apps/web/pages/account/billing.tsx @@ -1,8 +1,10 @@ +import { supabaseAdmin } from "@changespage/supabase/admin"; import { SpinnerWithSpacing } from "@changespage/ui"; import { DateTime } from "@changespage/utils"; -import { CurrencyDollarIcon } from "@heroicons/react/outline"; +import { CurrencyDollarIcon, DatabaseIcon } from "@heroicons/react/outline"; import { CalendarIcon } from "@heroicons/react/solid"; import classNames from "classnames"; +import { InferGetServerSidePropsType } from "next"; import { useEffect } from "react"; import { SecondaryButton } from "../../components/core/buttons.component"; import { notifyError, notifyInfo } from "../../components/core/toast.component"; @@ -10,9 +12,72 @@ import AuthLayout from "../../components/layout/auth-layout.component"; import Page from "../../components/layout/page.component"; import { ROUTES } from "../../data/routes.data"; import { httpPost } from "../../utils/http"; +import { withSupabase } from "../../utils/supabase/withSupabase"; import { useUserData } from "../../utils/useUser"; -export default function Billing() { +interface PageStorageUsage { + page_id: string; + page_title: string; + total_bytes: number; + total_pretty: string; +} + +function formatBytes(bytes: number): string { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; +} + +export const getServerSideProps = withSupabase(async (_, { user }) => { + const { data: pages } = await supabaseAdmin + .from("pages") + .select("id, title") + .eq("user_id", user.id); + + if (!pages || pages.length === 0) { + return { + props: { + storageUsage: [], + }, + }; + } + + const storageUsage: PageStorageUsage[] = await Promise.all( + pages.map(async (page) => { + const { data: objects } = await supabaseAdmin + // @ts-expect-error - storage schema not in Database types + .schema("storage") + .from("objects") + .select("metadata") + .eq("bucket_id", "images") + .like("name", `${user.id}/${page.id}/%`); + + const totalBytes = (objects || []).reduce((sum, obj) => { + const size = (obj.metadata as { size?: number })?.size || 0; + return sum + size; + }, 0); + + return { + page_id: page.id, + page_title: page.title, + total_bytes: totalBytes, + total_pretty: formatBytes(totalBytes), + }; + }) + ); + + return { + props: { + storageUsage, + }, + }; +}); + +export default function Billing({ + storageUsage, +}: InferGetServerSidePropsType) { const { billingDetails, fetchBilling } = useUserData(); async function openBillingPortal() { @@ -180,6 +245,52 @@ export default function Billing() { )}
+ +
+
+
+

+ Storage Usage +

+

+ Storage used by each page for images and uploads. +

+
+
+
+ {storageUsage.length === 0 ? ( +
+
+

+ No pages found. +

+
+
+ ) : ( +
+
+
    + {storageUsage.map((page) => ( +
  • +
    +
    + +

    + {page.page_title} +

    +
    +

    + {page.total_pretty} +

    +
    +
  • + ))} +
+
+
+ )} +
+
);