From d1d5263486f96db578aad918a59007045c3c077f Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 13 Jul 2025 09:28:24 +0000 Subject: feat: Add stripe based subscriptions --- apps/web/components/subscription/QuotaProgress.tsx | 177 +++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 apps/web/components/subscription/QuotaProgress.tsx (limited to 'apps/web/components/subscription') diff --git a/apps/web/components/subscription/QuotaProgress.tsx b/apps/web/components/subscription/QuotaProgress.tsx new file mode 100644 index 00000000..525eae8f --- /dev/null +++ b/apps/web/components/subscription/QuotaProgress.tsx @@ -0,0 +1,177 @@ +"use client"; + +import { useTranslation } from "@/lib/i18n/client"; +import { api } from "@/lib/trpc"; +import { Database, HardDrive } from "lucide-react"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "../ui/card"; +import { Progress } from "../ui/progress"; + +function formatBytes(bytes: number): string { + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; +} + +function formatNumber(num: number): string { + if (num >= 1000000) { + return (num / 1000000).toFixed(1) + "M"; + } + if (num >= 1000) { + return (num / 1000).toFixed(1) + "K"; + } + return num.toString(); +} + +interface QuotaProgressItemProps { + title: string; + icon: React.ReactNode; + used: number; + quota: number | null; + unlimited: boolean; + formatter: (value: number) => string; + description: string; +} + +function QuotaProgressItem({ + title, + icon, + used, + quota, + unlimited, + formatter, + description, +}: QuotaProgressItemProps) { + const { t } = useTranslation(); + const percentage = + unlimited || !quota ? 0 : Math.min((used / quota) * 100, 100); + const isNearLimit = percentage > 80; + const isAtLimit = percentage >= 100; + + return ( +
+
+ {icon} +

{title}

+
+ +
+
+ {description} + + {formatter(used)}{" "} + {unlimited + ? "" + : `/ ${quota !== null && quota !== undefined ? formatter(quota) : "∞"}`} + +
+ + {!unlimited && quota && ( + div]:bg-destructive" + : isNearLimit + ? "[&>div]:bg-orange-500" + : "" + }`} + /> + )} + + {unlimited && ( +
+ {t("settings.subscription.unlimited_usage")} +
+ )} + + {isAtLimit && ( +
+ {t("settings.subscription.quota_limit_reached")} +
+ )} + + {isNearLimit && !isAtLimit && ( +
+ {t("settings.subscription.approaching_quota_limit")} +
+ )} +
+
+ ); +} + +export function QuotaProgress() { + const { t } = useTranslation(); + const { data: quotaUsage, isLoading } = + api.subscriptions.getQuotaUsage.useQuery(); + + if (isLoading) { + return ( + + + {t("settings.subscription.usage_quotas")} + + {t("settings.subscription.loading_usage")} + + + +
+
+
+
+
+
+
+
+
+
+
+
+ ); + } + + if (!quotaUsage) { + return null; + } + + return ( + + + {t("settings.subscription.usage_quotas")} + + {t("settings.subscription.track_usage")} + + + + } + used={quotaUsage.bookmarks.used} + quota={quotaUsage.bookmarks.quota} + unlimited={quotaUsage.bookmarks.unlimited} + formatter={formatNumber} + description={t("settings.subscription.total_bookmarks_saved")} + /> + + } + used={quotaUsage.storage.used} + quota={quotaUsage.storage.quota} + unlimited={quotaUsage.storage.unlimited} + formatter={formatBytes} + description={t("settings.subscription.assets_file_storage")} + /> + + + ); +} -- cgit v1.2.3-70-g09d2