From d1d5263486f96db578aad918a59007045c3c077f Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
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/QuotaProgress.tsx')

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 (
+    <div className="space-y-3">
+      <div className="flex items-center gap-2">
+        {icon}
+        <h4 className="font-medium">{title}</h4>
+      </div>
+
+      <div className="space-y-2">
+        <div className="flex justify-between text-sm">
+          <span className="text-muted-foreground">{description}</span>
+          <span className={isAtLimit ? "font-medium text-destructive" : ""}>
+            {formatter(used)}{" "}
+            {unlimited
+              ? ""
+              : `/ ${quota !== null && quota !== undefined ? formatter(quota) : "∞"}`}
+          </span>
+        </div>
+
+        {!unlimited && quota && (
+          <Progress
+            value={percentage}
+            className={`h-2 ${
+              isAtLimit
+                ? "[&>div]:bg-destructive"
+                : isNearLimit
+                  ? "[&>div]:bg-orange-500"
+                  : ""
+            }`}
+          />
+        )}
+
+        {unlimited && (
+          <div className="text-xs text-muted-foreground">
+            {t("settings.subscription.unlimited_usage")}
+          </div>
+        )}
+
+        {isAtLimit && (
+          <div className="text-xs text-destructive">
+            {t("settings.subscription.quota_limit_reached")}
+          </div>
+        )}
+
+        {isNearLimit && !isAtLimit && (
+          <div className="text-xs text-orange-600">
+            {t("settings.subscription.approaching_quota_limit")}
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
+
+export function QuotaProgress() {
+  const { t } = useTranslation();
+  const { data: quotaUsage, isLoading } =
+    api.subscriptions.getQuotaUsage.useQuery();
+
+  if (isLoading) {
+    return (
+      <Card>
+        <CardHeader>
+          <CardTitle>{t("settings.subscription.usage_quotas")}</CardTitle>
+          <CardDescription>
+            {t("settings.subscription.loading_usage")}
+          </CardDescription>
+        </CardHeader>
+        <CardContent>
+          <div className="space-y-4">
+            <div className="animate-pulse space-y-2">
+              <div className="h-4 w-1/3 rounded bg-muted"></div>
+              <div className="h-2 rounded bg-muted"></div>
+            </div>
+            <div className="animate-pulse space-y-2">
+              <div className="h-4 w-1/3 rounded bg-muted"></div>
+              <div className="h-2 rounded bg-muted"></div>
+            </div>
+          </div>
+        </CardContent>
+      </Card>
+    );
+  }
+
+  if (!quotaUsage) {
+    return null;
+  }
+
+  return (
+    <Card>
+      <CardHeader>
+        <CardTitle>{t("settings.subscription.usage_quotas")}</CardTitle>
+        <CardDescription>
+          {t("settings.subscription.track_usage")}
+        </CardDescription>
+      </CardHeader>
+      <CardContent className="space-y-6">
+        <QuotaProgressItem
+          title="Bookmarks"
+          icon={<Database className="h-4 w-4" />}
+          used={quotaUsage.bookmarks.used}
+          quota={quotaUsage.bookmarks.quota}
+          unlimited={quotaUsage.bookmarks.unlimited}
+          formatter={formatNumber}
+          description={t("settings.subscription.total_bookmarks_saved")}
+        />
+
+        <QuotaProgressItem
+          title="Storage"
+          icon={<HardDrive className="h-4 w-4" />}
+          used={quotaUsage.storage.used}
+          quota={quotaUsage.storage.quota}
+          unlimited={quotaUsage.storage.unlimited}
+          formatter={formatBytes}
+          description={t("settings.subscription.assets_file_storage")}
+        />
+      </CardContent>
+    </Card>
+  );
+}
-- 
cgit v1.2.3-70-g09d2

