From d8e1854dbe61aecf1795a584fb5e2d923a7a6b7b Mon Sep 17 00:00:00 2001 From: Helix Y2J Date: Sun, 15 Feb 2026 16:18:51 +0530 Subject: [PATCH 1/5] bug: canfecth prNumber now --- app/api/webhooks/github/route.ts | 2 +- modules/ai/lib/rag.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/api/webhooks/github/route.ts b/app/api/webhooks/github/route.ts index 62e2aaf..8448bb9 100644 --- a/app/api/webhooks/github/route.ts +++ b/app/api/webhooks/github/route.ts @@ -12,7 +12,7 @@ export async function POST(req: NextRequest) { if (event == "pull_request") { const action = body.action; const repo = body.repository.full_name; - const prNumber = body.Number; + const prNumber = body.pull_request.number; const [owner, repoName] = repo.split("/"); reviewPullRequest(owner, repoName, prNumber) diff --git a/modules/ai/lib/rag.ts b/modules/ai/lib/rag.ts index 44135af..dda0b61 100644 --- a/modules/ai/lib/rag.ts +++ b/modules/ai/lib/rag.ts @@ -1,10 +1,11 @@ import { pineconeIndex } from "@/lib/pincecone"; import { embed } from "ai"; import { google } from "@ai-sdk/google"; +import "dotenv/config"; export async function generateEmbedding(text: string) { const { embedding } = await embed({ - model: google.embedding("text-embedding-04"), + model: google.embedding("gemini-embedding-001"), // 3072 dimensions, new state of art model as of 14th Jan, 2026 value: text }) return embedding From 9b950237c9522ee4beef8ee4261abbdf06c3b9aa Mon Sep 17 00:00:00 2001 From: Helix Y2J Date: Sun, 15 Feb 2026 18:19:52 +0530 Subject: [PATCH 2/5] feat: added review actions,pages #2 --- app/dashboard/reviews/page.tsx | 298 ++++++++++++++++++++++++++++++++ modules/review/actions/index.ts | 36 ++++ 2 files changed, 334 insertions(+) create mode 100644 app/dashboard/reviews/page.tsx create mode 100644 modules/review/actions/index.ts diff --git a/app/dashboard/reviews/page.tsx b/app/dashboard/reviews/page.tsx new file mode 100644 index 0000000..fc30704 --- /dev/null +++ b/app/dashboard/reviews/page.tsx @@ -0,0 +1,298 @@ +"use client"; + +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { toast } from "sonner"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"; +import Link from "next/link"; +import { useState, useMemo } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { getReviews } from "@/modules/review/actions"; +import { ExternalLink, Search, Filter, GitPullRequest, Calendar, CheckCircle2, Clock, AlertCircle } from "lucide-react"; + +type Review = { + id: string; + prNumber: number; + prTitle: string; + prUrl: string; + review: string; + status: string; + createdAt: Date; + repository: { + name: string; + fullName: string; + owner: string; + }; +}; + +export default function ReviewsPage() { + const { data: reviews, isLoading } = useQuery({ + queryKey: ["reviews"], + queryFn: getReviews + }); + + const [searchQuery, setSearchQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + + // Filter and search reviews + const filteredReviews = useMemo(() => { + if (!reviews) return []; + + return reviews.filter((review) => { + const matchesSearch = + review.prTitle.toLowerCase().includes(searchQuery.toLowerCase()) || + review.repository.name.toLowerCase().includes(searchQuery.toLowerCase()) || + review.prNumber.toString().includes(searchQuery); + + const matchesStatus = statusFilter === "all" || review.status === statusFilter; + + return matchesSearch && matchesStatus; + }); + }, [reviews, searchQuery, statusFilter]); + + const getStatusIcon = (status: string) => { + switch (status.toLowerCase()) { + case "completed": + return ; + case "pending": + return ; + case "failed": + return ; + default: + return ; + } + }; + + const getStatusColor = (status: string) => { + switch (status.toLowerCase()) { + case "completed": + return "bg-primary/10 text-primary border-primary/20"; + case "pending": + return "bg-accent/10 text-accent-foreground border-accent/20"; + case "failed": + return "bg-destructive/10 text-destructive border-destructive/20"; + default: + return "bg-muted text-muted-foreground border-border"; + } + }; + + const formatDate = (date: Date) => { + return new Date(date).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + hour: "2-digit", + minute: "2-digit" + }); + }; + + return ( +
+
+ {/* Header Section */} +
+
+

+ Code Reviews +

+

+ AI-powered reviews for your pull requests +

+
+ + {/* Search and Filter Bar */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-10 bg-card border-border focus:border-primary transition-colors" + /> +
+ +
+ +
+ + + +
+
+
+ + {/* Stats Bar */} +
+
+
+
+ +
+
+

{reviews?.length || 0}

+

Total Reviews

+
+
+
+ +
+
+
+ +
+
+

+ {reviews?.filter(r => r.status === "completed").length || 0} +

+

Completed

+
+
+
+ +
+
+
+ +
+
+

+ {reviews?.filter(r => r.status === "pending").length || 0} +

+

Pending

+
+
+
+
+
+ + {/* Reviews Grid */} + {isLoading ? ( +
+
+
+

Loading reviews...

+
+
+ ) : filteredReviews.length === 0 ? ( +
+
+
+ +
+
+

No reviews found

+

+ {searchQuery || statusFilter !== "all" + ? "Try adjusting your search or filters" + : "Reviews will appear here once PRs are analyzed"} +

+
+
+
+ ) : ( +
+ {filteredReviews.map((review) => ( + + +
+
+
+ + #{review.prNumber} + + + {getStatusIcon(review.status)} + {review.status} + +
+ + {review.prTitle} + + + + {review.repository.fullName} + + +
+ + + + +
+
+ + +
+

+
+ AI Review Summary +

+
+ {review.review.substring(0, 300)} + {review.review.length > 300 && "..."} +
+
+
+ + +
+
+ + {formatDate(review.createdAt)} +
+ +
+
+
+ ))} +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/modules/review/actions/index.ts b/modules/review/actions/index.ts new file mode 100644 index 0000000..3eac9d2 --- /dev/null +++ b/modules/review/actions/index.ts @@ -0,0 +1,36 @@ +"use server"; + + +import prisma from "@/lib/db"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; +import { inngest } from "@/inngest/client"; + +export const getReviews = async () => { + try { + const session = await auth.api.getSession({ + headers: await headers() + }) + if (!session?.user) { + throw new Error("Unauthorized") + } + const reviews = await prisma.review.findMany({ + where: { + repository: { + userId: session.user.id + } + }, + include: { + repository: true + }, + orderBy: { + createdAt: "desc" + }, + take: 50 + }) + return reviews + } catch (error) { + console.error("Failed to fetch reviews", error) + return [] + } +} \ No newline at end of file From f8b87cb4c85d1badee80c0b69279ffbd299ba3df Mon Sep 17 00:00:00 2001 From: Helix Y2J Date: Sun, 15 Feb 2026 22:47:15 +0530 Subject: [PATCH 3/5] feat: integrated polar --- lib/auth-client.ts | 2 + lib/auth.ts | 84 +++- modules/ai/action/index.ts | 7 + modules/payment/config/polar.ts | 6 + modules/payment/lib/subscription.ts | 204 ++++++++ modules/repository/actions/index.ts | 40 +- package-lock.json | 449 ++++++++++++++++++ package.json | 2 + .../migration.sql | 21 + .../migration.sql | 16 + prisma/schema.prisma | 17 + 11 files changed, 825 insertions(+), 23 deletions(-) create mode 100644 modules/payment/config/polar.ts create mode 100644 modules/payment/lib/subscription.ts create mode 100644 prisma/migrations/20260215125548_user_usage_added/migration.sql create mode 100644 prisma/migrations/20260215164256_updated_polar_id/migration.sql diff --git a/lib/auth-client.ts b/lib/auth-client.ts index cbf1375..c7309fb 100644 --- a/lib/auth-client.ts +++ b/lib/auth-client.ts @@ -1,5 +1,7 @@ import { createAuthClient } from "better-auth/react"; +import { polarClient } from "@polar-sh/better-auth"; export const { signIn, signUp, useSession, signOut } = createAuthClient({ baseURL: process.env.BETTER_AUTH_URL, + plugins: [polarClient()], }); \ No newline at end of file diff --git a/lib/auth.ts b/lib/auth.ts index 14e2ce7..828103f 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,16 +1,90 @@ import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; import prisma from "./db"; +import { polar, checkout, portal, usage, webhooks } from "@polar-sh/better-auth"; +import { Polar } from "@polar-sh/sdk"; +import { polarClient } from "@/modules/payment/config/polar"; +import { updatePolarCustomerId, updateUserTier } from "@/modules/payment/lib/subscription"; export const auth = betterAuth({ database: prismaAdapter(prisma, { provider: "postgresql", // or "mysq..etc }), socialProviders: { - github:{ - clientId:process.env.GITHUB_CLIENT_ID as string, - clientSecret:process.env.GITHUB_CLIENT_SECRET as string, - scope:["repo"] + github: { + clientId: process.env.GITHUB_CLIENT_ID as string, + clientSecret: process.env.GITHUB_CLIENT_SECRET as string, + scope: ["repo"] } - } + }, + plugins: [ + polar({ + client: polarClient, + createCustomerOnSignUp: true, + use: [ + checkout({ + products: [ + { + productId: "500d00ba-f6bf-43b9-8cb3-b492aa9b5842", + slug: "Code-council" + } + ], + successUrl: process.env.POLAR_SUCCESS_URL, + authenticatedUsersOnly: true + }), + portal({ + returnUrl: process.env.POLAR_RETURN_URL || "http://localhost:3002/dashbaord", + }), + usage(), + webhooks({ + secret: process.env.POLAR_WEBHOOK_SERCRET!, + onSubscriptionActive: async (payload) => { + const customerid = payload.data.customerId; + const user = await prisma.user.findUnique({ + where: { + polarCustomerID: customerid + } + }); + if (user) { + await updateUserTier(user.id, "PRO", "ACTIVE", payload.data.id) + } + }, + onSubscriptionCanceled: async (payload) => { + const customerid = payload.data.customerId; + const user = await prisma.user.findUnique({ + where: { + polarCustomerID: customerid + } + }); + if (user) { + await updateUserTier(user.id, user.subscriptionTier as any, "CANCELED") + } + }, + onSubscriptionRevoked: async (payload) => { + const customerid = payload.data.customerId; + const user = await prisma.user.findUnique({ + where: { + polarCustomerID: customerid + } + }); + if (user) { + await updateUserTier(user.id, "FREE", "EXPIRED") + } + }, + onOrderPaid: async () => { }, // later + onCustomerCreated: async (payload) => { + const user = await prisma.user.findUnique({ + where: { + email: payload.data.email + } + }); + if (user) { + await updatePolarCustomerId(user.id, payload.data.id) + } + }, + + }) + ], + }) + ] }); \ No newline at end of file diff --git a/modules/ai/action/index.ts b/modules/ai/action/index.ts index 1aa87c2..35ea2c8 100644 --- a/modules/ai/action/index.ts +++ b/modules/ai/action/index.ts @@ -4,6 +4,7 @@ import prisma from "@/lib/db"; import { retrieveContext } from "../lib/rag"; import { getPullRequestDiff } from "@/modules/github/lib/github"; import { inngest } from "@/inngest/client"; +import { canCreateReview, incrementReviewCount } from "@/modules/payment/lib/subscription"; export async function reviewPullRequest(owner: string, repo: string, prNumber: number) { try { @@ -29,6 +30,10 @@ export async function reviewPullRequest(owner: string, repo: string, prNumber: n throw new Error("Repository not found") } + const canReview = await canCreateReview(repository.user.id, repository.id); + if (!canReview) { + throw new Error("Peasant, You've reached the maximum number of reviews allowed in FREE tier") + } const githubAccount = repository.user.accounts[0]; if (!githubAccount?.accessToken) { throw new Error("Github account token not found for owner") @@ -43,6 +48,8 @@ export async function reviewPullRequest(owner: string, repo: string, prNumber: n userId: repository.user.id } }) + + await incrementReviewCount(repository.user.id, repository.id); return { success: true, message: "Review Queued" } } catch (error) { diff --git a/modules/payment/config/polar.ts b/modules/payment/config/polar.ts new file mode 100644 index 0000000..5742bc1 --- /dev/null +++ b/modules/payment/config/polar.ts @@ -0,0 +1,6 @@ +import { Polar } from "@polar-sh/sdk"; + +export const polarClient = new Polar({ + accessToken: process.env.POLAR_API_KEY, + server: "sandbox" +}); diff --git a/modules/payment/lib/subscription.ts b/modules/payment/lib/subscription.ts new file mode 100644 index 0000000..94053e8 --- /dev/null +++ b/modules/payment/lib/subscription.ts @@ -0,0 +1,204 @@ +"use server"; + +import prisma from "@/lib/db"; + +export type SubscriptionTier = "FREE" | "PRO"; +export type SubscriptionStatus = "ACTIVE" | "CANCELED" | "EXPIRED"; + +export interface UserLimits { + tier: SubscriptionTier; + repositories: { + current: number; + limit: number | null; + canAdd: boolean; + }; + reviews: { + [repositoryId: string]: { + current: number; + limit: number | null; + canAdd: boolean; + }; + }; + +} + +const TIER_LIMITS = { + FREE: { + repositories: 7, + reviewsPerRepo: 7, + }, + PRO: { + repositories: null, + reviewsPerRepo: null, + }, +} as const; + + + +export async function getUserTier(userId: string): Promise { + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { subscriptionTier: true }, + }); + + return (user?.subscriptionTier as SubscriptionTier) || "FREE"; +} + +async function getUserUsage(userId: string) { + let usage = await prisma.userUsage.findUnique({ + where: { userId }, + }); + + if (!usage) { + usage = await prisma.userUsage.create({ + data: { + userId, + repositoryCount: 0, + reviewCounts: {}, + }, + }); + } + + return usage; +} + +export async function canConnectRepository(userId: string): Promise { + const tier = await getUserTier(userId); + + if (tier === "PRO") { + return true; // Unlimited for pro users + } + + const usage = await getUserUsage(userId); + const limit = TIER_LIMITS.FREE.repositories; + + return usage.repositoryCount < limit; +} + + +export async function canCreateReview( + userId: string, + repositoryId: string +): Promise { + const tier = await getUserTier(userId); + + if (tier === "PRO") { + return true; // Unlimited for pro users + } + + const usage = await getUserUsage(userId); + const reviewCounts = usage.reviewCounts as Record; + const currentCount = reviewCounts[repositoryId] || 0; + const limit = TIER_LIMITS.FREE.reviewsPerRepo; + + return currentCount < limit; +} + +/** + * Increment repository count for user + */ +export async function incrementRepositoryCount(userId: string): Promise { + await prisma.userUsage.upsert({ + where: { userId }, + create: { + userId, + repositoryCount: 1, + reviewCounts: {}, + }, + update: { + repositoryCount: { + increment: 1, + }, + }, + }); +} + +export async function decrementRepositoryCount(userId: string): Promise { + const usage = await getUserUsage(userId); + + await prisma.userUsage.update({ + where: { userId }, + data: { + repositoryCount: Math.max(0, usage.repositoryCount - 1), + }, + }); +} + +export async function incrementReviewCount( + userId: string, + repositoryId: string +): Promise { + const usage = await getUserUsage(userId); + const reviewCounts = usage.reviewCounts as Record; + + reviewCounts[repositoryId] = (reviewCounts[repositoryId] || 0) + 1; + + await prisma.userUsage.update({ + where: { userId }, + data: { + reviewCounts, + }, + }); +} + +export async function getRemainingLimits(userId: string): Promise { + const tier = await getUserTier(userId); + const usage = await getUserUsage(userId); + const reviewCounts = usage.reviewCounts as Record; + + const limits: UserLimits = { + tier, + repositories: { + current: usage.repositoryCount, + limit: tier === "PRO" ? null : TIER_LIMITS.FREE.repositories, + canAdd: tier === "PRO" || usage.repositoryCount < TIER_LIMITS.FREE.repositories, + }, + reviews: {}, + }; + + // Get all user's repositories + const repositories = await prisma.repository.findMany({ + where: { userId }, + select: { id: true }, + }); + + // Calculate limits for each repository + for (const repo of repositories) { + const currentCount = reviewCounts[repo.id] || 0; + limits.reviews[repo.id] = { + current: currentCount, + limit: tier === "PRO" ? null : TIER_LIMITS.FREE.reviewsPerRepo, + canAdd: tier === "PRO" || currentCount < TIER_LIMITS.FREE.reviewsPerRepo, + }; + } + + return limits; +} + +export async function updateUserTier( + userId: string, + tier: SubscriptionTier, + status: SubscriptionStatus, + polarSubscriptionId?: string +): Promise { + await prisma.user.update({ + where: { id: userId }, + data: { + subscriptionTier: tier, + subscriptionStatus: status, + + }, + }); +} + +export async function updatePolarCustomerId( + userId: string, + polarCustomerID: string +): Promise { + await prisma.user.update({ + where: { id: userId }, + data: { + polarCustomerID + } + }) +} \ No newline at end of file diff --git a/modules/repository/actions/index.ts b/modules/repository/actions/index.ts index d448fe5..2ef231d 100644 --- a/modules/repository/actions/index.ts +++ b/modules/repository/actions/index.ts @@ -2,8 +2,7 @@ import prisma from "@/lib/db" import { auth } from "@/lib/auth" import { headers } from "next/headers" -import { redirect } from "next/navigation" -import { Octokit } from "octokit" +import { canConnectRepository, incrementRepositoryCount, decrementRepositoryCount } from "@/modules/payment/lib/subscription" import { getGithubAccessToken, getRepositories as getGithubRepositories, createWebhook } from "@/modules/github/lib/github" import { inngest } from "@/inngest/client" @@ -44,7 +43,11 @@ export const connectRepository = async (owner: string, repo: string, githubId: n } - // TDODO CHECK IF USER CAN CONNECT TO MORE REPO RO NOT + // DONE CHECK IF USER CAN CONNECT TO MORE REPO RO NOT + const canConnect = await canConnectRepository(session.user.id); + if (!canConnect) { + throw new Error("Peasant, Maximum number of repositories allowed in FREE tier is over. Upgrade to PRO tier for Unlimited repos.") + } const webhook = await createWebhook(owner, repo) @@ -60,25 +63,26 @@ export const connectRepository = async (owner: string, repo: string, githubId: n } }) - } - // TODO INCREMENET REPO COUNT FOR USAGE TRACKING - // TRIGGER REPO INDEXING FOR RAG + // DONE- INCREMENET REPO COUNT FOR USAGE TRACKING + await incrementRepositoryCount(session.user.id); - try { - await inngest.send({ - name: "repository.connected", - data: { - owner, - repo, - userId: session.user.id - } - }) - } catch (error) { - console.error("Failed to trigger repository indexing", error) - } + // TRIGGER REPO INDEXING FOR RAG + try { + await inngest.send({ + name: "repository.connected", + data: { + owner, + repo, + userId: session.user.id + } + }) + } catch (error) { + console.error("Failed to trigger repository indexing", error) + } + } return webhook diff --git a/package-lock.json b/package-lock.json index 055b6ba..658c94b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "@hookform/resolvers": "^5.2.2", "@phosphor-icons/react": "^2.1.10", "@pinecone-database/pinecone": "^7.0.0", + "@polar-sh/better-auth": "^1.8.1", + "@polar-sh/sdk": "^0.42.5", "@prisma/adapter-pg": "^7.3.0", "@prisma/client": "^6.19.2", "@radix-ui/react-accordion": "^1.2.12", @@ -3748,6 +3750,229 @@ "node": ">=20.0.0" } }, + "node_modules/@polar-sh/better-auth": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@polar-sh/better-auth/-/better-auth-1.8.1.tgz", + "integrity": "sha512-wZjF1xcxw1+blnR8JspPwZ0WhwoHkG4C2oFhVzb1RJKQfwTBjSmnrBftjaV6D3WFHDnrs0wFPnW/YHXzg8paOA==", + "dependencies": { + "@polar-sh/checkout": "^0.2.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@polar-sh/sdk": "^0.42.1", + "better-auth": "^1.4.12", + "zod": "^3.24.2 || ^4" + } + }, + "node_modules/@polar-sh/checkout": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polar-sh/checkout/-/checkout-0.2.0.tgz", + "integrity": "sha512-lkHa7JJtQpbHblsfpEX91bJRV6s7gyBIlehVCy2KRL89vP4t6t2vYKNwIxIHI+EG7RxXKu/4fCMchQYrVSMYuQ==", + "license": "Apache-2.0", + "dependencies": { + "@polar-sh/sdk": "^0.42.1", + "@polar-sh/ui": "^0.1.2", + "event-source-plus": "^0.1.15", + "eventemitter3": "^5.0.1", + "markdown-to-jsx": "^8.0.0", + "react-hook-form": "~7.70.0" + }, + "peerDependencies": { + "@stripe/react-stripe-js": "^3.6.0 || ^4.0.2", + "@stripe/stripe-js": "^7.1.0", + "react": "^18 || ^19" + } + }, + "node_modules/@polar-sh/checkout/node_modules/@polar-sh/ui": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@polar-sh/ui/-/ui-0.1.2.tgz", + "integrity": "sha512-YTmMB2lr+PplMTDZnTs0Crgu0KNBKyQcSX4N0FYXSlo1Q6e9IKs4hwzEcqNUv3eHS4BxGO1SvxxNjuSK+il49Q==", + "license": "Apache-2.0", + "dependencies": { + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toast": "^1.2.15", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-table": "^8.21.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "countries-list": "^3.2.0", + "date-fns": "^4.1.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.547.0", + "react-day-picker": "^9.11.1", + "react-hook-form": "^7.65.0", + "react-timeago": "^8.3.0", + "recharts": "^3.3.0", + "tailwind-merge": "^3.3.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, + "node_modules/@polar-sh/checkout/node_modules/@polar-sh/ui/node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@polar-sh/checkout/node_modules/@polar-sh/ui/node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@polar-sh/checkout/node_modules/@polar-sh/ui/node_modules/recharts": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", + "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@polar-sh/checkout/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/@polar-sh/checkout/node_modules/lucide-react": { + "version": "0.547.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.547.0.tgz", + "integrity": "sha512-YLChGBWKq8ynr1UWP8WWRPhHhyuBAXfSBnHSgfoj51L//9TU3d0zvxpigf5C1IJ4vnEoTzthl5awPK55PiZhdA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@polar-sh/checkout/node_modules/react-hook-form": { + "version": "7.70.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.70.0.tgz", + "integrity": "sha512-COOMajS4FI3Wuwrs3GPpi/Jeef/5W1DRR84Yl5/ShlT3dKVFUfoGiEZ/QE6Uw8P4T2/CLJdcTVYKvWBMQTEpvw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/@polar-sh/checkout/node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/@polar-sh/sdk": { + "version": "0.42.5", + "resolved": "https://registry.npmjs.org/@polar-sh/sdk/-/sdk-0.42.5.tgz", + "integrity": "sha512-GzC3/ElCtMO55+KeXwFTANlydZzw5qI3DU/F9vAFIsUKuegSmh+Xu03KCL+ct9/imJOvLUQucYhUSsNKqo2j2Q==", + "dependencies": { + "standardwebhooks": "^1.0.0", + "zod": "^3.25.65 || ^4.0.0" + } + }, "node_modules/@prisma/adapter-pg": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.3.0.tgz", @@ -5538,6 +5763,42 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -5545,6 +5806,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -5557,6 +5824,31 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@stripe/react-stripe-js": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-4.0.2.tgz", + "integrity": "sha512-l2wau+8/LOlHl+Sz8wQ1oDuLJvyw51nQCsu6/ljT6smqzTszcMHifjAJoXlnMfcou3+jK/kQyVe04u/ufyTXgg==", + "license": "MIT", + "peer": true, + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": ">=1.44.1 <8.0.0", + "react": ">=16.8.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.9.0.tgz", + "integrity": "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.16" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -5863,6 +6155,19 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@traceloop/ai-semantic-conventions": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/@traceloop/ai-semantic-conventions/-/ai-semantic-conventions-0.20.0.tgz", @@ -6172,6 +6477,12 @@ "@types/node": "*" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.53.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz", @@ -7572,6 +7883,12 @@ "dev": true, "license": "MIT" }, + "node_modules/countries-list": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.2.2.tgz", + "integrity": "sha512-ABJ/RWQBrPWy+hRuZoW+0ooK8p65Eo3WmUZwHm6v4wmfSPznNAKzjy3+UUYrJK2v3182BVsgWxdB6ROidj39kw==", + "license": "MIT" + }, "node_modules/cross-fetch": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", @@ -8205,6 +8522,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -8693,6 +9020,15 @@ "node": ">=0.10.0" } }, + "node_modules/event-source-plus": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/event-source-plus/-/event-source-plus-0.1.15.tgz", + "integrity": "sha512-kt3z/UwDbZxHttynwmXlqTf1qknWqPgswsbvSok1ob6SveMts4BqRXow6aiwB55xTY1XvSXuhn+IvYQErWLyKA==", + "license": "MIT", + "dependencies": { + "ofetch": "^1.5.1" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -8818,6 +9154,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -9338,6 +9680,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -10492,6 +10844,23 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-to-jsx": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-8.0.0.tgz", + "integrity": "sha512-hWEaRxeCDjes1CVUQqU+Ov0mCqBqkGhLKjL98KdbwHSgEWZZSJQeGlJQatVfeZ3RaxrfTrZZ3eczl2dhp5c/pA==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "react": ">= 0.14.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -10914,6 +11283,17 @@ "node": ">= 20" } }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", @@ -11477,6 +11857,29 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-remove-scroll": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", @@ -11571,6 +11974,15 @@ } } }, + "node_modules/react-timeago": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/react-timeago/-/react-timeago-8.3.0.tgz", + "integrity": "sha512-BeR0hj/5qqTc2+zxzBSQZMky6MmqwOtKseU3CSmcjKR5uXerej2QY34v2d+cdz11PoeVfAdWLX+qjM/UdZkUUg==", + "license": "MIT", + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -11638,6 +12050,21 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -11704,6 +12131,12 @@ "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -12113,6 +12546,16 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -12709,6 +13152,12 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, "node_modules/ulid": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.4.0.tgz", diff --git a/package.json b/package.json index f6928b3..ef93ef2 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "@hookform/resolvers": "^5.2.2", "@phosphor-icons/react": "^2.1.10", "@pinecone-database/pinecone": "^7.0.0", + "@polar-sh/better-auth": "^1.8.1", + "@polar-sh/sdk": "^0.42.5", "@prisma/adapter-pg": "^7.3.0", "@prisma/client": "^6.19.2", "@radix-ui/react-accordion": "^1.2.12", diff --git a/prisma/migrations/20260215125548_user_usage_added/migration.sql b/prisma/migrations/20260215125548_user_usage_added/migration.sql new file mode 100644 index 0000000..4413dfd --- /dev/null +++ b/prisma/migrations/20260215125548_user_usage_added/migration.sql @@ -0,0 +1,21 @@ +-- AlterTable +ALTER TABLE "user" ADD COLUMN "subscriptionStatus" TEXT, +ADD COLUMN "subscriptionTier" TEXT NOT NULL DEFAULT 'free'; + +-- CreateTable +CREATE TABLE "user_usage" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "repositoryCount" INTEGER NOT NULL DEFAULT 0, + "reviewCounts" JSONB NOT NULL DEFAULT '{}', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "user_usage_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "user_usage_userId_key" ON "user_usage"("userId"); + +-- AddForeignKey +ALTER TABLE "user_usage" ADD CONSTRAINT "user_usage_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20260215164256_updated_polar_id/migration.sql b/prisma/migrations/20260215164256_updated_polar_id/migration.sql new file mode 100644 index 0000000..386c17d --- /dev/null +++ b/prisma/migrations/20260215164256_updated_polar_id/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - A unique constraint covering the columns `[polarCustomerID]` on the table `user` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[polarSubscriptionId]` on the table `user` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "user" ADD COLUMN "polarCustomerID" TEXT, +ADD COLUMN "polarSubscriptionId" TEXT; + +-- CreateIndex +CREATE UNIQUE INDEX "user_polarCustomerID_key" ON "user"("polarCustomerID"); + +-- CreateIndex +CREATE UNIQUE INDEX "user_polarSubscriptionId_key" ON "user"("polarSubscriptionId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1f15065..e83bd4a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -30,6 +30,11 @@ model User { sessions Session[] accounts Account[] repositories Repository[] + userUsage UserUsage? + subscriptionTier String @default("free") + subscriptionStatus String? + polarCustomerID String? @unique + polarSubscriptionId String? @unique @@unique([email]) @@map("user") @@ -68,6 +73,18 @@ model Review { @@map("review") } +model UserUsage{ + id String @id @default(cuid()) + userId String @unique + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + repositoryCount Int @default(0) + reviewCounts Json @default("{}") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("user_usage") +} + model Session { id String @id expiresAt DateTime From e3e2cb53703d2d095cd5fa7f7d2ee5c250b610d2 Mon Sep 17 00:00:00 2001 From: Helix Y2J Date: Sun, 15 Feb 2026 23:57:22 +0530 Subject: [PATCH 4/5] bug: with checkout session --- app/dashboard/subscription/page.tsx | 468 ++++++++++++++++++++++++++++ components/ui/app-sidebar.tsx | 2 +- modules/auth/components/logout.tsx | 17 +- 3 files changed, 478 insertions(+), 9 deletions(-) create mode 100644 app/dashboard/subscription/page.tsx diff --git a/app/dashboard/subscription/page.tsx b/app/dashboard/subscription/page.tsx new file mode 100644 index 0000000..c3295f2 --- /dev/null +++ b/app/dashboard/subscription/page.tsx @@ -0,0 +1,468 @@ +"use client"; +import { customer, checkout } from "@/lib/auth-client"; +import { toast } from "sonner"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { getSubscriptionData, syncSubscriptionStatus } from "@/modules/payment/actions"; +import { useQuery } from "@tanstack/react-query"; +import { + SpinnerGap, + CheckCircle, + XCircle, + Crown, + Rocket, + GitBranch, + GitPullRequest, + ArrowRight, + ArrowsClockwise, + Gear, + Infinity, + Check +} from "@phosphor-icons/react"; +import { useState, useEffect } from "react"; +import { useSearchParams } from "next/navigation"; + +const PLAN_FEATURES = { + free: [ + { + name: "Up to 7 repositories", + included: true + }, + { + name: "Up to 7 reviews per repository", + included: true + }, + { + name: "Basic code review", + included: true + }, + { + name: "Deep code analysis", + included: true + }, + { + name: "Community support", + included: true + } + ], + pro: [ + { + name: "Unlimited repositories", + included: true + }, + { + name: "Unlimited reviews", + included: true + }, + { + name: "Advanced code review", + included: true + }, + { + name: "Email notifications", + included: true + }, + { + name: "Priority support", + included: true + } + ], +} + + +export default function SubscriptionPage() { + const [checkoutLoading, setCheckoutLoading] = useState(false); + const [portalLoading, setPortalLoading] = useState(false); + const [syncLoading, setSyncLoading] = useState(false); + const searchParams = useSearchParams(); + const success = searchParams.get("success"); + const [showSuccessAlert, setShowSuccessAlert] = useState(false); + + const { data, isLoading, error, refetch } = useQuery({ + queryKey: ["subscription-data"], + queryFn: getSubscriptionData, + refetchOnWindowFocus: true, + }); + + useEffect(() => { + if (success === "true") { + setShowSuccessAlert(true); + toast.success("Subscription updated successfully!"); + // Remove the success param from URL + window.history.replaceState({}, '', '/dashboard/subscription'); + // Refetch data to get updated subscription + refetch(); + } + }, [success, refetch]); + + const handleSync = async () => { + setSyncLoading(true); + try { + const result = await syncSubscriptionStatus(); + if (result.success) { + toast.success("Subscription synced successfully!"); + refetch(); + } else { + toast.error(result.message || "Failed to sync subscription"); + } + } catch (error) { + toast.error("Failed to sync subscription"); + } finally { + setSyncLoading(false); + } + }; + + const handleUpgrade = async () => { + setCheckoutLoading(true); + try { + // The checkout function should use the slug defined in auth.ts + // which maps to the productId: "500d00ba-f6bf-43b9-8cb3-b492aa9b5842" + const result = await checkout({ + productId: "500d00ba-f6bf-43b9-8cb3-b492aa9b5842", + slug: "Pro" + }); + + if (result?.url) { + window.location.href = result.url; + } else { + console.error("Checkout result:", result); + toast.error("Failed to create checkout session - no URL returned"); + } + } catch (error: any) { + console.error("Checkout error:", error); + toast.error(error?.message || "Failed to start checkout"); + } finally { + setCheckoutLoading(false); + } + }; + + const handleManageSubscription = async () => { + setPortalLoading(true); + try { + const result = await customer.portal(); + + if (result.url) { + window.location.href = result.url; + } else { + toast.error("Failed to open customer portal"); + } + } catch (error) { + console.error("Portal error:", error); + toast.error("Failed to open customer portal"); + } finally { + setPortalLoading(false); + } + }; + + if (isLoading) { + return ( +
+
+ +

Loading subscription data...

+
+
+ ) + } + + if (!data?.user) { + return ( +
+
+ +

Authentication Required

+

Please sign in to view subscription data

+
+
+ ) + } + + const currentTier = data.user.subscriptionTier as "FREE" | "PRO"; + const isPro = currentTier === "PRO"; + const isActive = data.user.subscriptionStatus === "ACTIVE"; + const limits = data.limits; + + return ( +
+ {/* Header */} +
+
+

+ Subscription Plans +

+

+ Manage your subscription and billing +

+
+ + +
+ + {/* Success Alert */} + {showSuccessAlert && ( + + + + Your subscription has been updated successfully! + + + )} + + {/* Current Plan Badge */} +
+ + +
+
+ {isPro ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+

+ {currentTier} Plan + {isPro && isActive && ( + + Active + + )} +

+

+ {isPro + ? "Unlimited access to all features" + : "Limited access with upgrade available"} +

+
+
+
+
+
+
+ + {/* Usage Statistics */} + {limits && ( +
+

Current Usage

+
+ {/* Repositories Usage */} + + + + + Repositories + + + +
+
+ {limits.repositories.current} + + / {limits.repositories.limit || ( + + )} + +
+
+
+
+

+ {limits.repositories.canAdd + ? "You can add more repositories" + : "Repository limit reached"} +

+
+ + + + {/* Reviews Usage */} + + + + + Reviews Per Repository + + + +
+
+ + {isPro ? ( + + ) : "7"} + + + {isPro ? "unlimited" : "per repo"} + +
+

+ {isPro + ? "Unlimited reviews for all repositories" + : "Up to 7 reviews per repository"} +

+
+
+
+
+
+ )} + + {/* Plan Cards */} +
+

Available Plans

+
+ {/* Free Plan */} + + {!isPro && ( +
+ + Current Plan + +
+ )} + +
+
+ +
+ Free +
+ + $0 + /month + +
+ +
    + {PLAN_FEATURES.free.map((feature, idx) => ( +
  • + + {feature.name} +
  • + ))} +
+
+ + + +
+ + {/* Pro Plan */} + +
+ {isPro && ( +
+ + Current Plan + +
+ )} + +
+
+ +
+ Pro +
+ + $10 + /month + +
+ +
    + {PLAN_FEATURES.pro.map((feature, idx) => ( +
  • + + {feature.name} +
  • + ))} +
+
+ + {isPro ? ( + + ) : ( + + )} + + +
+
+ + {/* Additional Info */} + + +
+ +
+

Need help?

+

+ Contact our support team if you have any questions about your subscription or billing. +

+
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/components/ui/app-sidebar.tsx b/components/ui/app-sidebar.tsx index 97cbfe6..2e64abc 100644 --- a/components/ui/app-sidebar.tsx +++ b/components/ui/app-sidebar.tsx @@ -54,7 +54,7 @@ export const AppSidebar = () => { { href: "/dashboard", label: "Dashboard", icon: GithubLogoIcon }, { href: "/dashboard/repository", label: "Repositories", icon: BookIcon }, { href: "/dashboard/reviews", label: "Reviews", icon: TreeViewIcon }, - { href: "/dashboard/subscriptions", label: "Subscriptions", icon: CoinsIcon }, + { href: "/dashboard/subscription", label: "Subscriptions", icon: CoinsIcon }, { href: "/dashboard/settings", label: "Settings", icon: GearSixIcon }, ] diff --git a/modules/auth/components/logout.tsx b/modules/auth/components/logout.tsx index ab49257..9ec96ac 100644 --- a/modules/auth/components/logout.tsx +++ b/modules/auth/components/logout.tsx @@ -1,7 +1,6 @@ "use client" import React from "react" import { signOut } from "@/lib/auth-client" -import { useRouter } from "next/navigation" const Logout = ({ children, @@ -10,15 +9,17 @@ const Logout = ({ children: React.ReactNode, className?: string }) => { - const router = useRouter() return ( - signOut({ - fetchOptions: { - onSuccess: () => { - router.push("/login") + { + await signOut({ + fetchOptions: { + onSuccess: () => { + // Use hard redirect to clear all cached state + window.location.href = "/login" + } } - } - })}> {children} + }) + }}> {children} ) } From 8f0116e5dfa018ff57538fdbff1f51567cebaf07 Mon Sep 17 00:00:00 2001 From: Helix Y2J Date: Tue, 17 Feb 2026 09:18:16 +0530 Subject: [PATCH 5/5] ui: improved charts UI --- README.md | 36 +------ components/ui/text-rotatot.tsx | 88 ++++++++++++++++++ modules/auth/components/login-ui.tsx | 62 ++++++------ .../dashboard/components/activity-chart.tsx | 21 ++--- package-lock.json | 69 ++++++++++++++ package.json | 1 + public/res/dashboard.png | Bin 0 -> 43058 bytes public/res/review.png | Bin 0 -> 69319 bytes 8 files changed, 206 insertions(+), 71 deletions(-) create mode 100644 components/ui/text-rotatot.tsx create mode 100644 public/res/dashboard.png create mode 100644 public/res/review.png diff --git a/README.md b/README.md index e215bc4..0fba44a 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,8 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Code council -## Getting Started +An AI-powered code review platform that uses RAG to analyze code diffs with repository aware context, post structured reviews on the PRs. -First, run the development server: -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` +![Dashboard](/public/res/dashboard.png) -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +![Review](/public/res/review.png) \ No newline at end of file diff --git a/components/ui/text-rotatot.tsx b/components/ui/text-rotatot.tsx new file mode 100644 index 0000000..306fe8d --- /dev/null +++ b/components/ui/text-rotatot.tsx @@ -0,0 +1,88 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import { motion } from 'framer-motion' + +type TextRotatorProps = { + words: string[] + className?: string + typingDelayMs?: number + deletingDelayMs?: number + holdAtFullMs?: number + holdAtEmptyMs?: number + showCursor?: boolean +} + +type Phase = 'typing' | 'deleting' | 'pause-full' | 'pause-empty' + +export function TextRotator({ + words, + className, + typingDelayMs = 120, + deletingDelayMs = 60, + holdAtFullMs = 1200, + holdAtEmptyMs = 400, + showCursor = true, +}: TextRotatorProps) { + const safeWords = useMemo(() => (words && words.length > 0 ? words.filter(Boolean) : ['']), [words]) + const [wordIndex, setWordIndex] = useState(0) + const [subIndex, setSubIndex] = useState(0) + const [phase, setPhase] = useState('typing') + const safeIndex = safeWords.length > 0 ? Math.min(wordIndex, safeWords.length - 1) : 0 + + // Reset wordIndex when words array shrinks and current index is out of bounds + useEffect(() => { + if (wordIndex >= safeWords.length && safeWords.length > 0) { + setWordIndex(0) + setSubIndex(0) + setPhase('typing') + } + }, [safeWords.length, wordIndex]) + + useEffect(() => { + const currentWord = safeWords[safeIndex] ?? '' + + let timeoutId: ReturnType | undefined + if (phase === 'typing') { + if (subIndex < currentWord.length) { + timeoutId = setTimeout(() => setSubIndex((s) => s + 1), typingDelayMs) + } else { + timeoutId = setTimeout(() => setPhase('pause-full'), holdAtFullMs) + } + } else if (phase === 'deleting') { + if (subIndex > 0) { + timeoutId = setTimeout(() => setSubIndex((s) => s - 1), deletingDelayMs) + } else { + timeoutId = setTimeout(() => setPhase('pause-empty'), holdAtEmptyMs) + } + } else if (phase === 'pause-full') { + timeoutId = setTimeout(() => setPhase('deleting'), deletingDelayMs) + } else if (phase === 'pause-empty') { + timeoutId = setTimeout(() => { + setWordIndex((i) => (i + 1) % Math.max(1, safeWords.length)) + setPhase('typing') + }, typingDelayMs) + } + + return () => { + if (timeoutId) clearTimeout(timeoutId) + } + }, [safeWords, safeIndex, wordIndex, subIndex, phase, typingDelayMs, deletingDelayMs, holdAtFullMs, holdAtEmptyMs]) + + const text = (safeWords[safeIndex] ?? '').slice(0, subIndex) + + return ( + + {text} + {showCursor && ( + + ) +} + diff --git a/modules/auth/components/login-ui.tsx b/modules/auth/components/login-ui.tsx index 0c343c2..afcfe5e 100644 --- a/modules/auth/components/login-ui.tsx +++ b/modules/auth/components/login-ui.tsx @@ -2,9 +2,9 @@ import React from "react" import { useState } from "react" import { signIn } from "@/lib/auth-client" -import { GithubLogoIcon, Atom, ShieldCheck } from '@phosphor-icons/react' +import { GithubLogoIcon, ShieldCheck } from '@phosphor-icons/react' import { Button } from "@/components/ui/button" -import { cn } from "@/lib/utils" +import { TextRotator } from "@/components/ui/text-rotatot" const LoginUI = () => { @@ -25,8 +25,11 @@ const LoginUI = () => { setLoading(false) } } + + const rotatorWords = ["before you merge", "written by AI", "before CI fails"] + return ( -
+
{/* Background Effects */}
@@ -34,30 +37,34 @@ const LoginUI = () => {
-
-
- - {/* Header */} -
-
- -
-
-

- Code Council -

-

- AI-powered code reviews for engineering teams. -

-
+
+ {/* Left: Branding */} +
+
+

+ Ship Production-Ready Code +

+

+ +

+

+ AI-powered code reviews for engineering teams. +

+
- {/* Auth Container */} + {/* Right: Auth */} +