diff --git a/app/(authenticated)/dashboard/page.tsx b/app/(authenticated)/dashboard/page.tsx index 099d6ae..3bef4a5 100644 --- a/app/(authenticated)/dashboard/page.tsx +++ b/app/(authenticated)/dashboard/page.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import Link from "next/link"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { Container } from "@/components/Container"; import { FadeIn, SlideIn } from "@/components/animations"; @@ -24,9 +24,9 @@ interface Tool { name: string; description: string; icon: string; + packagename?: string; user_id?: string; status?: string; - packagename?: string; version?: string; tool_categories?: Array<{ categories: { @@ -42,9 +42,17 @@ interface Tool { }; } +interface FailedToolUpdate { + id: string; + package_name: string; + version: string; + validation_warnings: string[] | null; +} + export default function DashboardPage() { const [user, setUser] = useState(null); const [tools, setTools] = useState([]); + const [failedToolUpdates, setFailedToolUpdates] = useState([]); const [loading, setLoading] = useState(true); const [sortBy, setSortBy] = useState<"downloads" | "rating" | "mau">("downloads"); const [viewMode, setViewMode] = useState<"all" | "my">("all"); @@ -79,12 +87,13 @@ export default function DashboardPage() { if (!response.ok) throw new Error("Failed to fetch dashboard data"); - const { user: dashUser, isAdmin: admin, tools: dashTools } = await response.json(); + const { user: dashUser, isAdmin: admin, tools: dashTools, failedToolUpdates: failedUpdates } = await response.json(); if (dashUser) { setUser(dashUser); setIsAdmin(admin); setTools(dashTools); + setFailedToolUpdates(failedUpdates || []); } } catch (error) { console.error("Error fetching dashboard data:", error); @@ -206,6 +215,9 @@ export default function DashboardPage() { } }); + // Precompute a Set of package names with failed updates for O(1) row lookups + const failedUpdatePackageNames = useMemo(() => new Set(failedToolUpdates.map((u) => u.package_name)), [failedToolUpdates]); + if (loading) { return (
@@ -358,6 +370,49 @@ export default function DashboardPage() { + {/* Failed Tool Updates Alert - shown only in My Tools view */} + {viewMode === "my" && failedToolUpdates.length > 0 && ( + +
+
+
+ + + +
+
+

+ {failedToolUpdates.length === 1 ? "1 tool update failed validation" : `${failedToolUpdates.length} tool updates failed validation`} +

+

+ The following tool{failedToolUpdates.length > 1 ? "s have" : " has"} a pending update that failed validation. Please review and fix the issues to + publish the update. +

+
    + {failedToolUpdates.map((update) => ( +
  • + {update.package_name} + {update.version && v{update.version}} + {update.validation_warnings && update.validation_warnings.length > 0 && ( + + ({update.validation_warnings[0]} + {update.validation_warnings.length > 1 ? ` +${update.validation_warnings.length - 1} more` : ""}) + + )} +
  • + ))} +
+
+
+
+
+ )} + {/* Tools Table */} {sortedTools.length === 0 ? ( @@ -400,6 +455,7 @@ export default function DashboardPage() { {sortedTools.map((tool) => { const analytics = tool.tool_analytics; + const toolHasFailedUpdate = viewMode === "my" && !!tool.packagename && failedUpdatePackageNames.has(tool.packagename); return ( @@ -412,7 +468,17 @@ export default function DashboardPage() { className="rounded" />
-
{tool.name}
+
+ {tool.name} + {toolHasFailedUpdate && ( + + Update failed + + )} +
{tool.description}
@@ -490,19 +556,36 @@ export default function DashboardPage() { <>
{ setOpenMoreMenuForToolId(null); moreMenuAnchorRef.current = null; }} - onKeyDown={(e) => { if (e.key === "Escape") { setOpenMoreMenuForToolId(null); moreMenuAnchorRef.current = null; } }} + onClick={() => { + setOpenMoreMenuForToolId(null); + moreMenuAnchorRef.current = null; + }} + onKeyDown={(e) => { + if (e.key === "Escape") { + setOpenMoreMenuForToolId(null); + moreMenuAnchorRef.current = null; + } + }} />
{ if (e.key === "Escape") { setOpenMoreMenuForToolId(null); moreMenuAnchorRef.current = null; } }} + style={ + moreMenuAnchorRef.current + ? { + top: moreMenuAnchorRef.current.bottom + 4, + left: moreMenuAnchorRef.current.right - 192, + } + : undefined + } + onKeyDown={(e) => { + if (e.key === "Escape") { + setOpenMoreMenuForToolId(null); + moreMenuAnchorRef.current = null; + } + }} > @@ -530,7 +618,12 @@ export default function DashboardPage() { className="flex w-full items-center gap-2 px-4 py-2 text-sm text-amber-700 hover:bg-amber-50 disabled:cursor-not-allowed disabled:text-slate-400" > - + {tool.status === TOOL_STATUSES.DEPRECATED ? "Deprecated" : "Deprecate"} @@ -546,7 +639,12 @@ export default function DashboardPage() { className="flex w-full items-center gap-2 px-4 py-2 text-sm text-red-600 hover:bg-red-50 disabled:cursor-not-allowed disabled:text-slate-400" > - + {tool.status === TOOL_STATUSES.DELETED ? "Deleted" : "Delete"} @@ -598,11 +696,18 @@ export default function DashboardPage() {
- +
-

Package Validation Failed

+

+ Package Validation Failed +

{validationModal.packageName}

@@ -637,7 +742,12 @@ export default function DashboardPage() { {validationModal.warnings.map((warn, i) => (
  • - + {warn}
  • diff --git a/app/api/dashboard/route.ts b/app/api/dashboard/route.ts index 4107a83..a091730 100644 --- a/app/api/dashboard/route.ts +++ b/app/api/dashboard/route.ts @@ -13,6 +13,36 @@ function getSupabaseClient() { return createClient(supabaseUrl, supabaseServiceKey); } +/** + * Compare two semver strings. Returns: + * > 0 if a > b + * 0 if a === b + * < 0 if a < b + * Non-numeric pre-release segments are compared lexicographically. + */ +function compareSemver(a: string, b: string): number { + const parse = (v: string) => + v + .replace(/^v/, "") + .split(".") + .map((p) => { + const n = parseInt(p, 10); + return isNaN(n) ? p : n; + }); + + const aParts = parse(a); + const bParts = parse(b); + const len = Math.max(aParts.length, bParts.length); + + for (let i = 0; i < len; i++) { + const ap = aParts[i] ?? 0; + const bp = bParts[i] ?? 0; + if (ap < bp) return -1; + if (ap > bp) return 1; + } + return 0; +} + export async function GET(request: NextRequest) { try { const supabase = getSupabaseClient(); @@ -56,6 +86,8 @@ export async function GET(request: NextRequest) { name, description, icon, + packagename, + version, user_id, status, packagename, @@ -81,10 +113,38 @@ export async function GET(request: NextRequest) { categories: tool.tool_categories?.map((tc: any) => tc.categories).filter(Boolean) || [], })) || []; + // Fetch failed tool updates for the authenticated user's tools + let failedToolUpdates: Array<{ id: string; package_name: string; version: string; validation_warnings: string[] | null }> = []; + if (user) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const userTools = (toolsData || []).filter((t: any) => t.user_id === user.id && t.packagename); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const userToolPackageNames = userTools.map((t: any) => t.packagename); + + if (userToolPackageNames.length > 0) { + const { data: failedUpdates } = await supabase + .from("tool_updates") + .select("id, package_name, version, validation_warnings") + .in("package_name", userToolPackageNames) + .eq("status", "validation_failed"); + + if (failedUpdates) { + // Only surface failed updates whose version is >= the tool's current published version + failedToolUpdates = failedUpdates.filter((update) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tool = userTools.find((t: any) => t.packagename === update.package_name); + if (!tool || !tool.version || !update.version) return true; + return compareSemver(update.version, tool.version) >= 0; + }); + } + } + } + return NextResponse.json({ user, isAdmin, tools: transformedTools, + failedToolUpdates, }); } catch (error) { console.error("Error fetching dashboard data:", error); diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.