From 40180f7250558d4dee564fcd65a1a3899ff6d159 Mon Sep 17 00:00:00 2001 From: Alice Soraya Date: Sat, 13 Jun 2026 11:50:02 +0700 Subject: [PATCH 01/15] feat: migrate to GraphQL and optimize performance --- package.json | 1 + pnpm-lock.yaml | 322 ++++++++++++++++++++++++++++++++ src/app/dashboard/page.tsx | 58 ++++-- src/app/page.tsx | 34 ++-- src/app/plugins/[slug]/page.tsx | 24 ++- src/lib/api.ts | 23 +++ 6 files changed, 429 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 2caeae6..bb648bb 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", + "sharp": "^0.35.1", "swr": "^2.4.1", "tailwindcss": "^4.3.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac14458..7245875 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: remark-gfm: specifier: ^4.0.1 version: 4.0.1 + sharp: + specifier: ^0.35.1 + version: 0.35.1 swr: specifier: ^2.4.1 version: 2.4.1(react@18.3.1) @@ -98,6 +101,9 @@ packages: '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} @@ -132,6 +138,168 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.1': + resolution: {integrity: sha512-T15JRWOubQ3f5+GxnWeIvo47u5qV0M9HBgJhT+f2gE1e9e6OhR6K73Re52Hm80qWcu1DNb3GweKmpr/MnuP2Ow==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.1': + resolution: {integrity: sha512-t1CPD0cr7XCHjwUj6tQ5MC0pCi866I+gUW6zbUX4aFPnKd1DFBtk0M+gWcjX8VeEzgfCNiSiNTVFZ6b7kvdbnQ==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.1': + resolution: {integrity: sha512-MBSQXqNPThW9EcZ905H6N4sEdX5EwZEYzGx5EBq9ncDCGJALMiY1xPFJxNdzuB1iBjLOpIfxajM6YxdvwmQSLA==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.0': + resolution: {integrity: sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.0': + resolution: {integrity: sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.0': + resolution: {integrity: sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.0': + resolution: {integrity: sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.0': + resolution: {integrity: sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.0': + resolution: {integrity: sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.0': + resolution: {integrity: sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.0': + resolution: {integrity: sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.0': + resolution: {integrity: sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.0': + resolution: {integrity: sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.1': + resolution: {integrity: sha512-ErCRyGU7LeoaFBZ0xW8hhLlXzhAg80sc4vxePB86qvtEvW1jEhhmbiNBP4oEzZfPMnu6HwHXfzD2W2kBU+RnCw==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.1': + resolution: {integrity: sha512-jygmR02PpCYypt7xB7nst1vqjZp/BpRA/Kf9nK7qRponJ/KrLPaZWEG4G15z1d2FZ6XqI+T0350ha3RSnKx24A==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.1': + resolution: {integrity: sha512-LUWZ2+r2UoLCd8j0RLCwQ4gL6w47+Y7igxtVnPIDXOOEjV86LpBkAHq5VpJeg+GHbw0KN/JWlPJOdZjyZnFqFQ==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.1': + resolution: {integrity: sha512-i7x6J3mwF4JgT0sM4V4WlAWdJ0bucPtA9rzO1bTji1n5qgBq/W5nn87RvOQPleuuxahNoLdTngByD8/vDDLArw==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.1': + resolution: {integrity: sha512-0zSaTUjTF0kIWTSYxD4EG/nvCU4jez53+3RdURtoY3HvbXtIQ98W90JnrGz/oLRFuEnfIy9+7xeq883euc0ZWw==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.1': + resolution: {integrity: sha512-NbJD4mWdeyrNQKluO/tR/wBDOelcowSVGNBWxI0e3ZtlXc6F/UOVKDj1MLD4zl3oHTuvKW3s+MA9N54YTldAYw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.1': + resolution: {integrity: sha512-VoW2sQCWI+0YIKQEmWJ8vzaQjTg9wIyfkFpvEfAS2h43X6iHu7GTk1hhOgB4IpSzCHe8UwQZIcx7b81VTaOrJA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.1': + resolution: {integrity: sha512-LjBoSd/c5JU0/K5MwzDMlgsSRP2bPn98JQGFFQAOLQ0bU/1z4ekxUdSKY9BmlwSh/cA+OrvpgsWqfZyYfVHBRw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.1': + resolution: {integrity: sha512-PCQUoQdZyE8tp3HpbevuihfUmgSP4qWI0FGEPWoeXqaS+cUrFfemabHQiebUmUmlUhCuNnQMxGrQ+CPqK4hnxg==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.1': + resolution: {integrity: sha512-xU2ml2bU2OPxYVvW2A6ae4M1g5QKyhKG06P4FAt+YEaFQQO0919Qx+XxIZEUuWTMoDViLpMws2/dQwoe/VcA6A==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.1': + resolution: {integrity: sha512-IkmHwuFhYpd3bTsN5SAahjwhiAcyXPooBt8vEUgxY3T0IP70sSJ0nU1xiPzZY8AH/OB1XpV3j8aZSVSOSfTbdA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.1': + resolution: {integrity: sha512-wQahqCi9MD8Yxzg4gVM4fNrZxh+r6vD55PyIg+WJPaM5ZRUyF35iQpwJCuma3r6viU9/8Pxlc+XHV+woVa6nCQ==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.1': + resolution: {integrity: sha512-WzBtkYtZHATLPe8XRharxZXxQ9cdLrQWHiwxt+BJ5rBsisQrKeeV86ErxPSVhcG6xCEuNhs0SqLpWr7XDa2k6w==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -2140,6 +2308,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} + engines: {node: '>=10'} + hasBin: true + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -2152,6 +2325,10 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + sharp@0.35.1: + resolution: {integrity: sha512-lW979AMi+ESidzMv/Lnv+F9bknzLyxLqFI05Sm433vOeRcltgxQmXpnfOOFIAlKtwXU/ksupm2srQoFCkR214g==} + engines: {node: '>=20.9.0'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2463,6 +2640,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 @@ -2503,6 +2685,112 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.35.1': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.0 + optional: true + + '@img/sharp-darwin-x64@0.35.1': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.0 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.1': + dependencies: + '@img/sharp-wasm32': 0.35.1 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.0': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.0': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.0': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.0': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.0': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.0': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.0': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.0': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.0': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.0': + optional: true + + '@img/sharp-linux-arm64@0.35.1': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.0 + optional: true + + '@img/sharp-linux-arm@0.35.1': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.0 + optional: true + + '@img/sharp-linux-ppc64@0.35.1': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.0 + optional: true + + '@img/sharp-linux-riscv64@0.35.1': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.0 + optional: true + + '@img/sharp-linux-s390x@0.35.1': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.0 + optional: true + + '@img/sharp-linux-x64@0.35.1': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.0 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.1': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.0 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.1': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.0 + optional: true + + '@img/sharp-wasm32@0.35.1': + dependencies: + '@emnapi/runtime': 1.11.1 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.1': + dependencies: + '@img/sharp-wasm32': 0.35.1 + optional: true + + '@img/sharp-win32-arm64@0.35.1': + optional: true + + '@img/sharp-win32-ia32@0.35.1': + optional: true + + '@img/sharp-win32-x64@0.35.1': + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -4861,6 +5149,8 @@ snapshots: semver@7.7.4: {} + semver@7.8.4: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -4883,6 +5173,38 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + sharp@0.35.1: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.1 + '@img/sharp-darwin-x64': 0.35.1 + '@img/sharp-freebsd-wasm32': 0.35.1 + '@img/sharp-libvips-darwin-arm64': 1.3.0 + '@img/sharp-libvips-darwin-x64': 1.3.0 + '@img/sharp-libvips-linux-arm': 1.3.0 + '@img/sharp-libvips-linux-arm64': 1.3.0 + '@img/sharp-libvips-linux-ppc64': 1.3.0 + '@img/sharp-libvips-linux-riscv64': 1.3.0 + '@img/sharp-libvips-linux-s390x': 1.3.0 + '@img/sharp-libvips-linux-x64': 1.3.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.0 + '@img/sharp-libvips-linuxmusl-x64': 1.3.0 + '@img/sharp-linux-arm': 0.35.1 + '@img/sharp-linux-arm64': 0.35.1 + '@img/sharp-linux-ppc64': 0.35.1 + '@img/sharp-linux-riscv64': 0.35.1 + '@img/sharp-linux-s390x': 0.35.1 + '@img/sharp-linux-x64': 0.35.1 + '@img/sharp-linuxmusl-arm64': 0.35.1 + '@img/sharp-linuxmusl-x64': 0.35.1 + '@img/sharp-webcontainers-wasm32': 0.35.1 + '@img/sharp-win32-arm64': 0.35.1 + '@img/sharp-win32-ia32': 0.35.1 + '@img/sharp-win32-x64': 0.35.1 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 351cb5c..83f13ea 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -11,7 +11,7 @@ import { import Link from "next/link"; import PluginImage from "@/components/PluginImage"; -import { fetchApi } from "@/lib/api"; +import { fetchGraphQL } from "@/lib/api"; import { getServerSession } from "next-auth/next"; import { authOptions } from "@/lib/auth"; import { redirect } from "next/navigation"; @@ -23,15 +23,48 @@ export default async function DashboardPage() { redirect("/api/auth/signin"); } - // Fetch real data from backend - const [pluginsRes, statsRes, statusRes] = await Promise.all([ - fetchApi("/api/v1/dashboard/plugins"), - fetchApi("/api/v1/dashboard/stats"), - fetchApi("/api/v1/dashboard/status"), - ]); + const GET_DASHBOARD_DATA = ` + query GetDashboardData { + dashboardStatus { + hasAppInstalled + githubTokenExpired + } + myStats { + totalPlugins + totalDownloads + totalVersions + pendingReviews + } + myPlugins { + id + slug + displayName + iconUrl + repoUrl + pluginType + downloads + status + latestVersion + } + } + `; - const hasAppInstalled = statusRes.data?.data?.hasAppInstalled || false; - const githubTokenExpired = statusRes.data?.data?.githubTokenExpired || false; + let hasAppInstalled = false; + let githubTokenExpired = false; + let stats = { totalPlugins: 0, totalDownloads: 0, totalVersions: 0, pendingReviews: 0 }; + let myPlugins: any[] = []; + + try { + const { data } = await fetchGraphQL(GET_DASHBOARD_DATA, {}, { cache: "no-store" }); + if (data) { + hasAppInstalled = data.dashboardStatus?.hasAppInstalled || false; + githubTokenExpired = data.dashboardStatus?.githubTokenExpired || false; + stats = data.myStats || stats; + myPlugins = data.myPlugins || []; + } + } catch (err) { + console.error("Failed to load dashboard data:", err); + } const installUrl = process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL || "https://github.com/apps/endgit-app/installations/new"; @@ -88,14 +121,7 @@ export default async function DashboardPage() { ); } - const stats = statsRes.data?.data || { - totalPlugins: 0, - totalDownloads: 0, - totalVersions: 0, - pendingReviews: 0, - }; - const myPlugins: any[] = pluginsRes.data?.data || []; return (
diff --git a/src/app/page.tsx b/src/app/page.tsx index 2a49e73..d3ed01c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -68,26 +68,36 @@ function HomeSkeleton() { ); } -async function HomeData() { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"; +import { fetchGraphQL } from "@/lib/api"; + +const GET_HOME_PLUGINS = ` + query GetHomePlugins { + homePlugins { + hotPlugins { id name slug displayName description iconUrl pluginType stars downloads commentCount isFeatured isPreRelease latestVersion author { username displayName avatarUrl } } + newPlugins { id name slug displayName description iconUrl pluginType stars downloads commentCount isFeatured isPreRelease latestVersion author { username displayName avatarUrl } } + topPlugins { id name slug displayName description iconUrl pluginType stars downloads commentCount isFeatured isPreRelease latestVersion author { username displayName avatarUrl } } + featuredPlugins { id name slug displayName description iconUrl pluginType stars downloads commentCount isFeatured isPreRelease latestVersion author { username displayName avatarUrl } } + } + } +`; +async function HomeData() { let hotPlugins: any[] = []; let newPlugins: any[] = []; let topPlugins: any[] = []; let featuredPlugins: any[] = []; try { - const res = await fetch(`${apiUrl}/api/v1/plugins/home`, { - next: { revalidate: 60 }, - }); - const json = await res.json(); - if (json.success) { - hotPlugins = json.data.hotPlugins || []; - newPlugins = json.data.newPlugins || []; - topPlugins = json.data.topPlugins || []; - featuredPlugins = json.data.featuredPlugins || []; + const { data } = await fetchGraphQL(GET_HOME_PLUGINS, {}, { revalidate: 60, noAuth: true }); + if (data?.homePlugins) { + hotPlugins = data.homePlugins.hotPlugins || []; + newPlugins = data.homePlugins.newPlugins || []; + topPlugins = data.homePlugins.topPlugins || []; + featuredPlugins = data.homePlugins.featuredPlugins || []; } - } catch {} + } catch (err) { + console.error("Failed to load home plugins:", err); + } return ( import("@/components/VirusTotalCard"), export const dynamic = "force-dynamic"; +const GET_PLUGIN = ` + query GetPlugin($slug: String!) { + plugin(slug: $slug) { + id name slug displayName description longDescription iconUrl repoUrl license tags keywords pluginType downloads stars commentCount heatScore status qualityBadge isVerified isFeatured createdAt updatedAt + author { id username displayName avatarUrl bio } + versions { id version changelog longDescription fileName fileSize fileHash minApiVersion supportedApis downloads isLatest isPreRelease status statusReason createdAt producers { githubUser role } virustotal { scanId status malicious suspicious undetected total permalink scanDate } } + } + } +`; + async function getPlugin(slug: string) { - const { data } = await fetchApi(`/api/v1/plugins/${slug}`); - return data?.data || null; + try { + const { data } = await fetchGraphQL(GET_PLUGIN, { slug }, { revalidate: 60, noAuth: true }); + return data?.plugin || null; + } catch (err) { + return null; + } } export async function generateMetadata({ @@ -258,8 +272,8 @@ export default async function PluginDetailPage({ {/* Temp Debug Panel */}

diff --git a/src/lib/api.ts b/src/lib/api.ts index dbe87e5..93a9dc7 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -54,3 +54,26 @@ export async function fetchApi( return { response, data }; } + +/** + * Helper function to execute GraphQL queries/mutations. + */ +export async function fetchGraphQL( + query: string, + variables: Record = {}, + options: FetchApiOptions = {} +) { + const { response, data } = await fetchApi("/api/graphql", { + ...options, + method: "POST", + body: JSON.stringify({ query, variables }), + }); + + if (data?.errors) { + console.error("GraphQL Errors:", data.errors); + throw new Error(data.errors[0]?.message || "GraphQL Error"); + } + + return { response, data: data?.data }; +} + From e34cf614f8a1a74b9792b702b828eb35943f7416 Mon Sep 17 00:00:00 2001 From: Alice Soraya Date: Sat, 13 Jun 2026 11:55:11 +0700 Subject: [PATCH 02/15] fix(chart): prevent decimal numbers in downloads YAxis (fixes #34) --- src/components/PluginAnalyticsChart.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/PluginAnalyticsChart.tsx b/src/components/PluginAnalyticsChart.tsx index f19201f..3e824e9 100644 --- a/src/components/PluginAnalyticsChart.tsx +++ b/src/components/PluginAnalyticsChart.tsx @@ -122,6 +122,7 @@ export default function PluginAnalyticsChart({ slug }: { slug: string }) { interval="preserveStartEnd" /> Date: Sat, 13 Jun 2026 12:00:15 +0700 Subject: [PATCH 03/15] fix: pass filter down to backend API to fix Dev Dashboard pagination and load time --- src/app/dashboard/dev/page.tsx | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/app/dashboard/dev/page.tsx b/src/app/dashboard/dev/page.tsx index 3837dd2..daecbf3 100644 --- a/src/app/dashboard/dev/page.tsx +++ b/src/app/dashboard/dev/page.tsx @@ -89,8 +89,8 @@ export default function DevDashboardPage() { useEffect(() => { if (!initialFetchDone.current) return; if (sessionStatus !== "authenticated") return; - fetchRepos(1, selectedOrg, debouncedSearch); - }, [debouncedSearch]); + fetchRepos(1, selectedOrg, debouncedSearch, filter); + }, [debouncedSearch, filter]); useEffect(() => { const handleClickOutside = (e: MouseEvent) => { @@ -141,6 +141,7 @@ export default function DevDashboardPage() { pageNumber: number = 1, org: string | null = selectedOrg, searchQuery: string = debouncedSearch, + currentFilter: string = filter, ) => { if (pageNumber === 1) setLoading(true); else setIsFetchingMore(true); @@ -179,8 +180,11 @@ export default function DevDashboardPage() { const searchParam = searchQuery ? `&search=${encodeURIComponent(searchQuery)}` : ""; + const filterParam = currentFilter !== "all" + ? `&filter=${encodeURIComponent(currentFilter)}` + : ""; const res = await fetch( - `${apiUrl}/api/v1/github/repos?page=${pageNumber}&per_page=10${orgParam}${searchParam}`, + `${apiUrl}/api/v1/github/repos?page=${pageNumber}&per_page=10${orgParam}${searchParam}${filterParam}`, { headers: token ? { Authorization: `Bearer ${token}` } : {}, }, @@ -221,7 +225,7 @@ export default function DevDashboardPage() { const handleOrgChange = (orgLogin: string | null) => { setSelectedOrg(orgLogin); setOrgDropdownOpen(false); - fetchRepos(1, orgLogin, debouncedSearch); + fetchRepos(1, orgLogin, debouncedSearch, filter); }; const toggleCI = async (repo: Repo) => { @@ -285,7 +289,7 @@ export default function DevDashboardPage() { } // Refresh current repos by reloading page 1 - await fetchRepos(1, selectedOrg, debouncedSearch); + await fetchRepos(1, selectedOrg, debouncedSearch, filter); } catch (err: any) { alert(`An error occurred while toggling CI: ${err.message}`); } finally { @@ -293,13 +297,7 @@ export default function DevDashboardPage() { } }; - const filteredRepos = repos.filter((r) => { - if (filter === "enabled" && !r.ciEnabled) return false; - if (filter === "disabled" && r.ciEnabled) return false; - if (search && !r.name.toLowerCase().includes(search.toLowerCase())) - return false; - return true; - }); + const filteredRepos = repos; const enabledCount = repos.filter((r) => r.ciEnabled).length; const disabledCount = repos.filter((r) => !r.ciEnabled).length; From 31115800e23912dceffea97680913decacfd7af2 Mon Sep 17 00:00:00 2001 From: Alice Soraya Date: Thu, 18 Jun 2026 18:43:43 +0700 Subject: [PATCH 04/15] chore: remove stars from UI and fix plugin card author layout --- src/app/page.tsx | 8 ++++---- src/app/plugins/[slug]/page.tsx | 2 +- src/components/LatestPluginsSection.tsx | 1 - src/components/PluginCardGrid.tsx | 27 +++++++++++++------------ 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index d3ed01c..d5085db 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -73,10 +73,10 @@ import { fetchGraphQL } from "@/lib/api"; const GET_HOME_PLUGINS = ` query GetHomePlugins { homePlugins { - hotPlugins { id name slug displayName description iconUrl pluginType stars downloads commentCount isFeatured isPreRelease latestVersion author { username displayName avatarUrl } } - newPlugins { id name slug displayName description iconUrl pluginType stars downloads commentCount isFeatured isPreRelease latestVersion author { username displayName avatarUrl } } - topPlugins { id name slug displayName description iconUrl pluginType stars downloads commentCount isFeatured isPreRelease latestVersion author { username displayName avatarUrl } } - featuredPlugins { id name slug displayName description iconUrl pluginType stars downloads commentCount isFeatured isPreRelease latestVersion author { username displayName avatarUrl } } + hotPlugins { id name slug displayName description iconUrl pluginType downloads commentCount isFeatured isPreRelease latestVersion author { username displayName avatarUrl } } + newPlugins { id name slug displayName description iconUrl pluginType downloads commentCount isFeatured isPreRelease latestVersion author { username displayName avatarUrl } } + topPlugins { id name slug displayName description iconUrl pluginType downloads commentCount isFeatured isPreRelease latestVersion author { username displayName avatarUrl } } + featuredPlugins { id name slug displayName description iconUrl pluginType downloads commentCount isFeatured isPreRelease latestVersion author { username displayName avatarUrl } } } } `; diff --git a/src/app/plugins/[slug]/page.tsx b/src/app/plugins/[slug]/page.tsx index bc560b2..14d0513 100644 --- a/src/app/plugins/[slug]/page.tsx +++ b/src/app/plugins/[slug]/page.tsx @@ -69,7 +69,7 @@ export const dynamic = "force-dynamic"; const GET_PLUGIN = ` query GetPlugin($slug: String!) { plugin(slug: $slug) { - id name slug displayName description longDescription iconUrl repoUrl license tags keywords pluginType downloads stars commentCount heatScore status qualityBadge isVerified isFeatured createdAt updatedAt + id name slug displayName description longDescription iconUrl repoUrl license tags keywords pluginType downloads commentCount heatScore status qualityBadge isVerified isFeatured createdAt updatedAt author { id username displayName avatarUrl bio } versions { id version changelog longDescription fileName fileSize fileHash minApiVersion supportedApis downloads isLatest isPreRelease status statusReason createdAt producers { githubUser role } virustotal { scanId status malicious suspicious undetected total permalink scanDate } } } diff --git a/src/components/LatestPluginsSection.tsx b/src/components/LatestPluginsSection.tsx index 25037a4..f409040 100644 --- a/src/components/LatestPluginsSection.tsx +++ b/src/components/LatestPluginsSection.tsx @@ -20,7 +20,6 @@ interface Plugin { iconUrl?: string; repoUrl?: string; latestVersion?: string; - stars?: number; downloads?: number; commentCount?: number; heatScore?: number; diff --git a/src/components/PluginCardGrid.tsx b/src/components/PluginCardGrid.tsx index 8e688cf..a11207b 100644 --- a/src/components/PluginCardGrid.tsx +++ b/src/components/PluginCardGrid.tsx @@ -24,7 +24,6 @@ interface Plugin { iconUrl?: string; repoUrl?: string; latestVersion?: string; - stars?: number; downloads?: number; commentCount?: number; heatScore?: number; @@ -155,6 +154,7 @@ function PluginCard({ {plugin.downloads?.toLocaleString() ?? 0} + {(plugin.heatScore || 0) > 0 && ( {plugin.heatScore} @@ -176,7 +176,7 @@ function PluginCard({ return (

-
+

{plugin.displayName}

-
- {plugin.isPreRelease && } - {isVerified && } - {isFeatured && } -
-
+
{author}
-
+
+ {plugin.isPreRelease && } + {isVerified && } + {isFeatured && } +
+
{plugin.downloads?.toLocaleString() ?? 0} + {(plugin.heatScore || 0) > 0 && ( {plugin.heatScore} @@ -222,7 +223,7 @@ function PluginCard({ {plugin.commentCount} )} - + v{plugin.latestVersion || "1.0.0"}
@@ -280,7 +281,7 @@ export default function PluginCardGrid({ plugins }: { plugins: Plugin[] }) {
From 51907319641acfd595d847073a82498c011b0ba3 Mon Sep 17 00:00:00 2001 From: Alice Soraya Date: Thu, 18 Jun 2026 19:02:54 +0700 Subject: [PATCH 05/15] feat: improve version selector UI and date formatting --- src/components/VersionSelector.tsx | 67 +++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/src/components/VersionSelector.tsx b/src/components/VersionSelector.tsx index a678b9f..8f6cb57 100644 --- a/src/components/VersionSelector.tsx +++ b/src/components/VersionSelector.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useRef, useEffect } from "react"; import { Download } from "lucide-react"; import { useRouter, useSearchParams } from "next/navigation"; @@ -8,9 +8,15 @@ interface Version { id: string; version: string; fileSize: number; - createdAt: string; + createdAt: string | number; } +const parseDate = (dateVal: string | number) => { + if (!dateVal) return "Unknown"; + const d = new Date(Number.isNaN(Number(dateVal)) ? dateVal : Number(dateVal)); + return isNaN(d.getTime()) ? "Unknown" : d.toLocaleDateString(); +}; + interface Props { slug: string; pluginType?: string; @@ -28,6 +34,23 @@ export default function VersionSelector({ const [selectedVersionStr, setSelectedVersionStr] = useState( activeVersionStr || versions[0]?.version, ); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsDropdownOpen(false); + } + }; + if (isDropdownOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [isDropdownOpen]); if (!versions || versions.length === 0) { return ( @@ -64,19 +87,31 @@ export default function VersionSelector({ ))} {pluginType === "CPP" ? ( - ) : ( Size:{" "} {selectedVersion.fileSize - ? `${(selectedVersion.fileSize / 1024).toFixed(0)} KB` + ? `${(selectedVersion.fileSize / (1024 * 1024)).toFixed(2)} MB` : "—"} - Uploaded: {new Date(selectedVersion.createdAt).toLocaleDateString()} + Uploaded: {parseDate(selectedVersion.createdAt)}
From d6dd87b51824f2f7e147d3ddfd33c3af369cdd68 Mon Sep 17 00:00:00 2001 From: Alice Soraya Date: Thu, 18 Jun 2026 19:20:32 +0700 Subject: [PATCH 06/15] fix: removed virustotal scan and resolved syntax error --- src/app/plugins/[slug]/page.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/app/plugins/[slug]/page.tsx b/src/app/plugins/[slug]/page.tsx index 14d0513..2c6d73a 100644 --- a/src/app/plugins/[slug]/page.tsx +++ b/src/app/plugins/[slug]/page.tsx @@ -55,14 +55,6 @@ const PluginDiscussion = nextDynamic( ), }, ); -const VirusTotalCard = nextDynamic(() => import("@/components/VirusTotalCard"), { - ssr: false, - loading: () => ( -
-

Loading security scan...

-
- ), -}); export const dynamic = "force-dynamic"; @@ -71,7 +63,7 @@ const GET_PLUGIN = ` plugin(slug: $slug) { id name slug displayName description longDescription iconUrl repoUrl license tags keywords pluginType downloads commentCount heatScore status qualityBadge isVerified isFeatured createdAt updatedAt author { id username displayName avatarUrl bio } - versions { id version changelog longDescription fileName fileSize fileHash minApiVersion supportedApis downloads isLatest isPreRelease status statusReason createdAt producers { githubUser role } virustotal { scanId status malicious suspicious undetected total permalink scanDate } } + versions { id version changelog longDescription fileName fileSize fileHash minApiVersion supportedApis downloads isLatest isPreRelease status statusReason createdAt producers { githubUser role } } } } `; @@ -440,8 +432,7 @@ export default async function PluginDetailPage({ {/* Right Sidebar */}
diff --git a/src/app/shield.api/[slug]/route.ts b/src/app/shield.api/[slug]/route.ts index e3f204a..9dcb292 100644 --- a/src/app/shield.api/[slug]/route.ts +++ b/src/app/shield.api/[slug]/route.ts @@ -8,21 +8,32 @@ export async function GET( try { const plugin = await fetchPluginData(params.slug); if (!plugin) { - return new NextResponse(createBadgeSvg("unknown @endgit", "not found", "#e05d44"), { headers: { "Content-Type": "image/svg+xml" } }); + return new NextResponse( + createBadgeSvg("unknown @endgit", "not found", "#e05d44"), + { headers: { "Content-Type": "image/svg+xml" } }, + ); } - const latestVersion = plugin.versions?.find((v: any) => v.isLatest) || plugin.versions?.[0]; - const versionLabel = latestVersion ? `v${latestVersion.version} @endgit` : `unknown @endgit`; - + const latestVersion = + plugin.versions?.find((v: any) => v.isLatest) || plugin.versions?.[0]; + const versionLabel = latestVersion + ? `v${latestVersion.version} @endgit` + : `unknown @endgit`; + let apis = "unknown"; if (latestVersion?.supportedApis?.length) { apis = latestVersion.supportedApis.join(", "); } return new NextResponse(createBadgeSvg(versionLabel, apis, "#97ca00"), { - headers: { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=3600" }, + headers: { + "Content-Type": "image/svg+xml", + "Cache-Control": "public, max-age=3600", + }, }); } catch (error) { - return new NextResponse(createBadgeSvg("error", "error", "#e05d44"), { headers: { "Content-Type": "image/svg+xml" } }); + return new NextResponse(createBadgeSvg("error", "error", "#e05d44"), { + headers: { "Content-Type": "image/svg+xml" }, + }); } } diff --git a/src/app/shield.dl.total/[slug]/route.ts b/src/app/shield.dl.total/[slug]/route.ts index ee7a246..c41a066 100644 --- a/src/app/shield.dl.total/[slug]/route.ts +++ b/src/app/shield.dl.total/[slug]/route.ts @@ -8,15 +8,26 @@ export async function GET( try { const plugin = await fetchPluginData(params.slug); if (!plugin) { - return new NextResponse(createBadgeSvg("endgit", "not found", "#e05d44"), { headers: { "Content-Type": "image/svg+xml" } }); + return new NextResponse( + createBadgeSvg("endgit", "not found", "#e05d44"), + { headers: { "Content-Type": "image/svg+xml" } }, + ); } let dl = plugin.downloads || 0; - return new NextResponse(createBadgeSvg("endgit", `${dl} downloads total`, "#97ca00"), { - headers: { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=3600" }, - }); + return new NextResponse( + createBadgeSvg("endgit", `${dl} downloads total`, "#97ca00"), + { + headers: { + "Content-Type": "image/svg+xml", + "Cache-Control": "public, max-age=3600", + }, + }, + ); } catch (error) { - return new NextResponse(createBadgeSvg("endgit", "error", "#e05d44"), { headers: { "Content-Type": "image/svg+xml" } }); + return new NextResponse(createBadgeSvg("endgit", "error", "#e05d44"), { + headers: { "Content-Type": "image/svg+xml" }, + }); } } diff --git a/src/app/shield.dl/[slug]/route.ts b/src/app/shield.dl/[slug]/route.ts index ed8be02..6409fbd 100644 --- a/src/app/shield.dl/[slug]/route.ts +++ b/src/app/shield.dl/[slug]/route.ts @@ -8,18 +8,32 @@ export async function GET( try { const plugin = await fetchPluginData(params.slug); if (!plugin) { - return new NextResponse(createBadgeSvg("unknown @endgit", "not found", "#e05d44"), { headers: { "Content-Type": "image/svg+xml" } }); + return new NextResponse( + createBadgeSvg("unknown @endgit", "not found", "#e05d44"), + { headers: { "Content-Type": "image/svg+xml" } }, + ); } - const latestVersion = plugin.versions?.find((v: any) => v.isLatest) || plugin.versions?.[0]; - const versionLabel = latestVersion ? `v${latestVersion.version}@endgit` : `unknown@endgit`; - + const latestVersion = + plugin.versions?.find((v: any) => v.isLatest) || plugin.versions?.[0]; + const versionLabel = latestVersion + ? `v${latestVersion.version}@endgit` + : `unknown@endgit`; + let dl = latestVersion?.downloads || 0; - return new NextResponse(createBadgeSvg(versionLabel, `${dl} downloads`, "#dfb317"), { - headers: { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=3600" }, - }); + return new NextResponse( + createBadgeSvg(versionLabel, `${dl} downloads`, "#dfb317"), + { + headers: { + "Content-Type": "image/svg+xml", + "Cache-Control": "public, max-age=3600", + }, + }, + ); } catch (error) { - return new NextResponse(createBadgeSvg("error", "error", "#e05d44"), { headers: { "Content-Type": "image/svg+xml" } }); + return new NextResponse(createBadgeSvg("error", "error", "#e05d44"), { + headers: { "Content-Type": "image/svg+xml" }, + }); } } diff --git a/src/app/shield.state/[slug]/route.ts b/src/app/shield.state/[slug]/route.ts index b4dcd14..4096922 100644 --- a/src/app/shield.state/[slug]/route.ts +++ b/src/app/shield.state/[slug]/route.ts @@ -8,11 +8,17 @@ export async function GET( try { const plugin = await fetchPluginData(params.slug); if (!plugin) { - return new NextResponse(createBadgeSvg("unknown @endgit", "not found", "#e05d44"), { headers: { "Content-Type": "image/svg+xml" } }); + return new NextResponse( + createBadgeSvg("unknown @endgit", "not found", "#e05d44"), + { headers: { "Content-Type": "image/svg+xml" } }, + ); } - const latestVersion = plugin.versions?.find((v: any) => v.isLatest) || plugin.versions?.[0]; - const versionLabel = latestVersion ? `v${latestVersion.version} @endgit` : `unknown @endgit`; + const latestVersion = + plugin.versions?.find((v: any) => v.isLatest) || plugin.versions?.[0]; + const versionLabel = latestVersion + ? `v${latestVersion.version} @endgit` + : `unknown @endgit`; const status = latestVersion ? latestVersion.status : plugin.status; let color = "#9ca3af"; // gray @@ -30,9 +36,14 @@ export async function GET( } return new NextResponse(createBadgeSvg(versionLabel, message, color), { - headers: { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=3600" }, + headers: { + "Content-Type": "image/svg+xml", + "Cache-Control": "public, max-age=3600", + }, }); } catch (error) { - return new NextResponse(createBadgeSvg("error", "error", "#e05d44"), { headers: { "Content-Type": "image/svg+xml" } }); + return new NextResponse(createBadgeSvg("error", "error", "#e05d44"), { + headers: { "Content-Type": "image/svg+xml" }, + }); } } diff --git a/src/lib/badge.ts b/src/lib/badge.ts index 9f8a684..6c15d5a 100644 --- a/src/lib/badge.ts +++ b/src/lib/badge.ts @@ -1,6 +1,10 @@ import { fetchGraphQL } from "@/lib/api"; -export function createBadgeSvg(label: string, message: string, color: string): string { +export function createBadgeSvg( + label: string, + message: string, + color: string, +): string { const labelWidth = Math.round(label.length * 6.5) + 14; const messageWidth = Math.round(message.length * 6.5) + 14; const totalWidth = labelWidth + messageWidth; @@ -44,7 +48,7 @@ export async function fetchPluginData(slug: string) { } `, { slug }, - { noAuth: true } + { noAuth: true }, ); return data?.plugin || null; } catch (error) {