Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 32 additions & 13 deletions apps/web/components/marketing/pricing-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -36,7 +52,7 @@ export default function PricingSection({ unit_amount = 500, addons = [] }) {
Simple Pricing
</h2>
<p className="mt-2 text-4xl font-bold tracking-tight text-white hero">
Everything you need for just ${Number(unit_amount) / 100 || "5"}{" "}
Everything you need for just ${priceInDollars}
</p>
</div>
</div>
Expand Down Expand Up @@ -77,11 +93,11 @@ export default function PricingSection({ unit_amount = 500, addons = [] }) {
</p>
</div>
<div className="-mt-2 p-2 lg:mt-0 lg:w-full lg:max-w-md lg:flex-shrink-0">
<div className="rounded-2xl bg-gray-950 py-6 text-center ring-1 ring-inset ring-gray-900/5 lg:flex lg:flex-col lg:justify-center lg:py-16">
<div className="rounded-2xl bg-gray-950 py-6 text-center ring-1 ring-inset ring-gray-900/5 lg:flex lg:flex-col lg:justify-center lg:py-20">
<div className="mx-auto max-w-xs px-8">
<div className="mt-6 flex items-baseline justify-center gap-x-2">
<p className="hero mt-4 flex items-baseline text-5xl font-bold tracking-tight text-gray-100">
${Number(unit_amount / 100)}
${priceInDollars}
<span className="text-lg font-semibold leading-8 tracking-normal text-gray-400">
/page /mo
</span>
Expand Down Expand Up @@ -116,6 +132,9 @@ export default function PricingSection({ unit_amount = 500, addons = [] }) {
>
Start free trial
</Link>
<p className="text-xs mt-2 text-gray-400">
14-days free trial
</p>
</div>
</div>
</div>
Expand Down
115 changes: 113 additions & 2 deletions apps/web/pages/account/billing.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,83 @@
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";
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<typeof getServerSideProps>) {
const { billingDetails, fetchBilling } = useUserData();

async function openBillingPortal() {
Expand Down Expand Up @@ -180,6 +245,52 @@ export default function Billing() {
)}
</div>
</div>

<div className="md:grid md:grid-cols-3 md:gap-6 mt-10">
<div className="md:col-span-1">
<div className="px-4 sm:px-0">
<h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-50">
Storage Usage
</h3>
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
Storage used by each page for images and uploads.
</p>
</div>
</div>
<div className="mt-5 md:mt-0 md:col-span-2">
{storageUsage.length === 0 ? (
<div className="shadow overflow-hidden sm:rounded-md">
<div className="px-4 py-3 bg-white dark:bg-black sm:p-3">
<p className="text-sm text-gray-500 dark:text-gray-400">
No pages found.
</p>
</div>
</div>
) : (
<div className="shadow overflow-hidden sm:rounded-md">
<div className="bg-white dark:bg-black">
<ul className="divide-y divide-gray-200 dark:divide-gray-800">
{storageUsage.map((page) => (
<li key={page.page_id} className="px-4 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center">
<DatabaseIcon className="h-5 w-5 text-gray-400 mr-3" />
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
{page.page_title}
</p>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400">
{page.total_pretty}
</p>
</div>
</li>
))}
</ul>
</div>
</div>
)}
</div>
</div>
</div>
</Page>
);
Expand Down