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 ? (
+
+ ) : (
+
+
+
+ {storageUsage.map((page) => (
+ -
+
+
+
+
+ {page.page_title}
+
+
+
+ {page.total_pretty}
+
+
+
+ ))}
+
+
+
+ )}
+
+
);