diff --git a/apps/web/pages/account/billing.tsx b/apps/web/pages/account/billing.tsx index 1a3b050..c1ae6d6 100644 --- a/apps/web/pages/account/billing.tsx +++ b/apps/web/pages/account/billing.tsx @@ -1,7 +1,11 @@ import { supabaseAdmin } from "@changespage/supabase/admin"; import { SpinnerWithSpacing } from "@changespage/ui"; import { DateTime } from "@changespage/utils"; -import { CurrencyDollarIcon, DatabaseIcon } from "@heroicons/react/outline"; +import { + CurrencyDollarIcon, + DatabaseIcon, + EyeIcon, +} from "@heroicons/react/outline"; import { CalendarIcon } from "@heroicons/react/solid"; import classNames from "classnames"; import { InferGetServerSidePropsType } from "next"; @@ -15,11 +19,12 @@ import { httpPost } from "../../utils/http"; import { withSupabase } from "../../utils/supabase/withSupabase"; import { useUserData } from "../../utils/useUser"; -interface PageStorageUsage { +interface PageUsageStats { page_id: string; page_title: string; total_bytes: number; total_pretty: string; + page_views_30d: number; } function formatBytes(bytes: number): string { @@ -39,22 +44,31 @@ export const getServerSideProps = withSupabase(async (_, { user }) => { if (!pages || pages.length === 0) { return { props: { - storageUsage: [], + usageStats: [], }, }; } - const storageUsage: PageStorageUsage[] = await Promise.all( + const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + + const usageStats: PageUsageStats[] = 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 [storageResult, viewsResult] = await Promise.all([ + 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}/%`), + supabaseAdmin + .from("page_views") + .select("id", { count: "exact", head: true }) + .eq("page_id", page.id) + .gte("created_at", thirtyDaysAgo.toISOString()), + ]); - const totalBytes = (objects || []).reduce((sum, obj) => { + const totalBytes = (storageResult.data || []).reduce((sum, obj) => { const size = (obj.metadata as { size?: number })?.size || 0; return sum + size; }, 0); @@ -64,19 +78,20 @@ export const getServerSideProps = withSupabase(async (_, { user }) => { page_title: page.title, total_bytes: totalBytes, total_pretty: formatBytes(totalBytes), + page_views_30d: viewsResult.count || 0, }; }) ); return { props: { - storageUsage, + usageStats, }, }; }); export default function Billing({ - storageUsage, + usageStats, }: InferGetServerSidePropsType) { const { billingDetails, fetchBilling } = useUserData(); @@ -250,15 +265,15 @@ export default function Billing({

- Storage Usage + Usage

- Storage used by each page for images and uploads. + Storage and page views (last 30 days) for each page.

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

@@ -270,18 +285,20 @@ export default function Billing({

    - {storageUsage.map((page) => ( + {usageStats.map((page) => (
  • -
    +

    + {page.page_title} +

    +
    - -

    - {page.page_title} -

    -
    -

    + {page.total_pretty} -

    +
    +
    + + {page.page_views_30d.toLocaleString()} views +
  • ))} diff --git a/apps/web/pages/changelog.tsx b/apps/web/pages/changelog.tsx index aa79968..b5b6a78 100644 --- a/apps/web/pages/changelog.tsx +++ b/apps/web/pages/changelog.tsx @@ -10,6 +10,13 @@ import HeaderComponent from "../components/layout/header.component"; import { Spinner } from "@changespage/ui"; const POSTS_PER_PAGE = 10; +const UNDERLINE_COLORS = [ + "decoration-blue-500", + "decoration-yellow-500", + "decoration-red-500", + "decoration-indigo-500", + "decoration-green-500", +]; const client = createChangesPageClient({ baseUrl: "https://hey.changes.page", @@ -38,14 +45,14 @@ export default function Changelog({

    - {posts.map((post) => ( + {posts.map((post, index) => ( {({ title, content, tags, formattedDate }) => ( -
    +
    -

    +

    {title}