Skip to content
Open
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
4 changes: 2 additions & 2 deletions apps/cursor/src/actions/review-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const approvePluginAction = adminActionClient

const { error } = await supabase
.from("plugins")
.update({ active: true })
.update({ active: true, status: "approved" })
.eq("id", pluginId);

if (error) {
Expand Down Expand Up @@ -70,7 +70,7 @@ export const declinePluginAction = adminActionClient

const { error } = await supabase
.from("plugins")
.delete()
.update({ active: false, status: "declined" })
.eq("id", pluginId);

if (error) {
Expand Down
20 changes: 10 additions & 10 deletions apps/cursor/src/app/admin/plugins/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { getPendingPlugins } from "@/data/queries";
import { getDeclinedPlugins, getPendingPlugins } from "@/data/queries";
import { isAdmin } from "@/utils/admin";
import { getSession } from "@/utils/supabase/auth";
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { PluginReviewList } from "./plugin-review-list";
import { PluginReviewTabs } from "./plugin-review-tabs";

export const metadata: Metadata = {
title: "Review Plugins | Admin",
Expand All @@ -16,22 +16,22 @@ export default async function AdminPluginsPage() {
redirect("/");
}

const { data: plugins } = await getPendingPlugins({
since: "2026-03-16T00:00:00Z",
});
const [{ data: plugins }, { data: declined }] = await Promise.all([
getPendingPlugins({ since: "2026-03-16T00:00:00Z" }),
getDeclinedPlugins({ since: "2026-03-16T00:00:00Z" }),
]);

return (
<div className="min-h-screen px-6 pt-24 md:pt-32 pb-32">
<div className="mx-auto w-full max-w-3xl">
<div className="mb-10">
<h1 className="marketing-page-title mb-3">Review Plugins</h1>
<p className="marketing-copy text-muted-foreground">
{plugins?.length ?? 0} pending{" "}
{plugins?.length === 1 ? "submission" : "submissions"}
</p>
</div>

<PluginReviewList plugins={plugins ?? []} />
<PluginReviewTabs
pending={plugins ?? []}
declined={declined ?? []}
/>
</div>
</div>
);
Expand Down
46 changes: 27 additions & 19 deletions apps/cursor/src/app/admin/plugins/plugin-review-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import { useAction } from "next-safe-action/hooks";
import { useState } from "react";
import { toast } from "sonner";

function PluginReviewCard({ plugin }: { plugin: PluginRow }) {
function PluginReviewCard({
plugin,
variant = "pending",
}: { plugin: PluginRow; variant?: "pending" | "declined" }) {
const [dismissed, setDismissed] = useState(false);

const { execute: approve, isExecuting: isApproving } = useAction(
Expand All @@ -32,7 +35,7 @@ function PluginReviewCard({ plugin }: { plugin: PluginRow }) {
declinePluginAction,
{
onSuccess: () => {
toast.success(`"${plugin.name}" declined and removed.`);
toast.success(`"${plugin.name}" declined.`);
setDismissed(true);
},
onError: ({ error }) => {
Expand All @@ -50,7 +53,7 @@ function PluginReviewCard({ plugin }: { plugin: PluginRow }) {
];

return (
<div className="rounded-lg border border-border bg-card p-5 shadow-cursor">
<div className={`rounded-lg border p-5 shadow-cursor ${variant === "declined" ? "border-border/50 bg-card/50 opacity-75" : "border-border bg-card"}`}>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 flex-1">
<Link
Expand Down Expand Up @@ -104,19 +107,21 @@ function PluginReviewCard({ plugin }: { plugin: PluginRow }) {
</div>

<div className="flex shrink-0 items-center gap-2">
<Button
variant="outline"
size="sm"
disabled={busy}
onClick={() => decline({ pluginId: plugin.id })}
>
{isDeclining ? (
<Loader2 className="size-3.5 animate-spin" />
) : (
<Trash2 className="size-3.5" />
)}
<span className="ml-1.5">Decline</span>
</Button>
{variant === "pending" && (
<Button
variant="outline"
size="sm"
disabled={busy}
onClick={() => decline({ pluginId: plugin.id })}
>
{isDeclining ? (
<Loader2 className="size-3.5 animate-spin" />
) : (
<Trash2 className="size-3.5" />
)}
<span className="ml-1.5">Decline</span>
</Button>
)}
<Button
size="sm"
disabled={busy}
Expand All @@ -135,12 +140,15 @@ function PluginReviewCard({ plugin }: { plugin: PluginRow }) {
);
}

export function PluginReviewList({ plugins }: { plugins: PluginRow[] }) {
export function PluginReviewList({
plugins,
variant = "pending",
}: { plugins: PluginRow[]; variant?: "pending" | "declined" }) {
if (plugins.length === 0) {
return (
<div className="rounded-lg border border-border bg-card p-10 text-center shadow-cursor">
<p className="text-sm text-muted-foreground">
No pending plugins to review.
No {variant} plugins to review.
</p>
</div>
);
Expand All @@ -149,7 +157,7 @@ export function PluginReviewList({ plugins }: { plugins: PluginRow[] }) {
return (
<div className="space-y-3">
{plugins.map((plugin) => (
<PluginReviewCard key={plugin.id} plugin={plugin} />
<PluginReviewCard key={plugin.id} plugin={plugin} variant={variant} />
))}
</div>
);
Expand Down
45 changes: 45 additions & 0 deletions apps/cursor/src/app/admin/plugins/plugin-review-tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import type { PluginRow } from "@/data/queries";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
import { PluginReviewList } from "./plugin-review-list";

export function PluginReviewTabs({
pending,
declined,
}: {
pending: PluginRow[];
declined: PluginRow[];
}) {
return (
<Tabs defaultValue="pending">
<TabsList>
<TabsTrigger value="pending">
Pending{" "}
<span className="ml-1.5 text-xs text-muted-foreground">
{pending.length}
</span>
</TabsTrigger>
<TabsTrigger value="declined">
Declined{" "}
<span className="ml-1.5 text-xs text-muted-foreground">
{declined.length}
</span>
</TabsTrigger>
</TabsList>

<TabsContent value="pending">
<PluginReviewList plugins={pending} variant="pending" />
</TabsContent>

<TabsContent value="declined">
<PluginReviewList plugins={declined} variant="declined" />
</TabsContent>
</Tabs>
);
}
34 changes: 34 additions & 0 deletions apps/cursor/src/data/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ export type PluginRow = {
author_avatar: string | null;
owner_id: string | null;
active: boolean;
status: "pending" | "approved" | "declined";
plan: string;
order: number;
install_count: number;
Expand Down Expand Up @@ -452,6 +453,39 @@ export async function getPendingPlugins({
.from("plugins")
.select("*, plugin_components(*)")
.eq("active", false)
.eq("status", "pending")
.order("created_at", { ascending: false })
.range(from, from + PAGE_SIZE - 1);

if (since) {
query = query.gte("created_at", since);
}

const { data, error } = await query;
if (error) return { data: allData.length ? allData : null, error };
if (!data || data.length === 0) break;

allData = allData.concat(data as PluginRow[]);
if (data.length < PAGE_SIZE) break;
from += PAGE_SIZE;
}

return { data: allData as PluginRow[], error: null };
}

export async function getDeclinedPlugins({
since,
}: { since?: string } = {}) {
const supabase = await createClient();
const PAGE_SIZE = 100;
let allData: PluginRow[] = [];
let from = 0;

while (true) {
let query = supabase
.from("plugins")
.select("*, plugin_components(*)")
.eq("status", "declined")
.order("created_at", { ascending: false })
.range(from, from + PAGE_SIZE - 1);

Expand Down