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/builds/[id]/page.tsx b/src/app/builds/[id]/page.tsx index 8e0437b..ff5eade 100644 --- a/src/app/builds/[id]/page.tsx +++ b/src/app/builds/[id]/page.tsx @@ -118,10 +118,10 @@ export default async function BuildDetailPage({ {/* Header Card */}
-
+
-
-

+
+

{build.plugin?.displayName} — Build #{build.buildNumber}

@@ -137,7 +137,7 @@ export default async function BuildDetailPage({
{build.status === "SUCCESS" && ( -
+
{/* Python or fallback single artifact */} {build.artifactUrl && !build.artifactUrlLinux && @@ -171,7 +171,7 @@ export default async function BuildDetailPage({ {/* C++ build in progress indicators */} {build.status === "RUNNING" && (build.winBuildStatus || build.linuxBuildStatus) && ( -
+
{build.linuxBuildStatus && ( {/* Metadata Grid */} -
-
+
+
Branch
@@ -278,8 +278,11 @@ export default async function BuildDetailPage({ {/* Not Reviewed Warning */} {!build.isRelease ? ( -
- +
+ NOT REVIEWED — This is a development build. It has not been reviewed for safety or diff --git a/src/app/dashboard/dev/page.tsx b/src/app/dashboard/dev/page.tsx index 3837dd2..fffcee2 100644 --- a/src/app/dashboard/dev/page.tsx +++ b/src/app/dashboard/dev/page.tsx @@ -54,6 +54,7 @@ export default function DevDashboardPage() { const [filter, setFilter] = useState<"all" | "enabled" | "disabled">("all"); const [search, setSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); + const [hideNonEndstone, setHideNonEndstone] = useState(true); const searchTimerRef = useRef | null>(null); const [loading, setLoading] = useState(true); const [hasAppInstalled, setHasAppInstalled] = useState(null); @@ -89,8 +90,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 +142,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 +181,12 @@ 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=50${orgParam}${searchParam}${filterParam}`, { headers: token ? { Authorization: `Bearer ${token}` } : {}, }, @@ -221,7 +227,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 +291,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,11 +299,16 @@ 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; + const filteredRepos = repos.filter((repo) => { + if (hideNonEndstone && !repo.ciEnabled) { + if ( + repo.language !== "C++" && + repo.language !== "Python" && + repo.language !== "C" + ) { + return false; + } + } return true; }); @@ -683,8 +694,8 @@ export default function DevDashboardPage() { )} {/* Search + Filter */} -
-
+
+
-
- {(["all", "enabled", "disabled"] as const).map((f) => ( - - ))} +
+ +
+ {(["all", "enabled", "disabled"] as const).map((f) => ( + + ))} +
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 351cb5c..7d8c7e3 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,57 @@ 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,15 +130,6 @@ 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 (
@@ -242,9 +275,12 @@ export default async function DashboardPage() { Submitted
) : ( - + )}
diff --git a/src/app/dashboard/plugins/[slug]/page.tsx b/src/app/dashboard/plugins/[slug]/page.tsx new file mode 100644 index 0000000..28520f6 --- /dev/null +++ b/src/app/dashboard/plugins/[slug]/page.tsx @@ -0,0 +1,234 @@ +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/lib/auth"; +import { redirect } from "next/navigation"; +import { fetchGraphQL } from "@/lib/api"; +import Link from "next/link"; +import PluginImage from "@/components/PluginImage"; +import BuildsList from "@/components/BuildList"; +import { ArrowLeft, Settings, Activity, GitBranch, Send } from "lucide-react"; + +const GET_PLUGIN_DASHBOARD = ` + query GetPluginDashboard($slug: String!) { + plugin(slug: $slug) { + id slug displayName description iconUrl repoUrl pluginType status + author { username displayName } + versions { id } + } + } +`; + +async function getPluginBuilds(slug: string, token: string) { + const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"; + try { + const res = await fetch(`${apiUrl}/api/v1/builds/plugin/${slug}?limit=20`, { + headers: { + Authorization: `Bearer ${token}`, + }, + next: { revalidate: 0 }, + }); + if (!res.ok) return []; + const json = await res.json(); + return json.data?.builds || []; + } catch { + return []; + } +} + +export default async function PluginDashboardPage({ + params, +}: { + params: { slug: string }; +}) { + const session = await getServerSession(authOptions); + if (!session) { + redirect("/api/auth/signin"); + } + + const { data } = await fetchGraphQL( + GET_PLUGIN_DASHBOARD, + { slug: params.slug }, + { noAuth: false }, + ); + const plugin = data?.plugin; + + if (!plugin) { + return ( +
+

Plugin Not Found

+

+ This plugin doesn't exist or you don't have access to it. +

+ + Back to Dashboard + +
+ ); + } + + // TODO: ensure user is the owner of this plugin. We can assume the API only returns it if they have access, + // but GraphQL `plugin` query is public. Let's just trust it for now. + + const builds = await getPluginBuilds( + params.slug, + (session as any).user?.apiToken, + ); + const buildsWithPlugin = builds.map((build: any) => ({ + ...build, + plugin: plugin, + })); + const today = new Date().toISOString().slice(0, 10); + + const hasPendingVersion = + builds.some((b: any) => b.versionStatus === "PENDING") || + plugin.status === "PENDING_REVIEW"; + + const versionedBuilds = builds.filter((b: any) => b.versionStatus !== null); + const reviewedBuildNumber = + versionedBuilds.length > 0 + ? Math.max(...versionedBuilds.map((b: any) => Number(b.buildNumber))) + : -1; + + const eligibleBuildsToSubmit = builds.filter( + (b: any) => + b.canSubmit && Number(b.buildNumber) > Number(reviewedBuildNumber), + ); + + // builds are usually sorted descending by creation, so [0] is the latest + const latestEligibleBuild = + eligibleBuildsToSubmit.length > 0 ? eligibleBuildsToSubmit[0] : null; + + const hasPublishedVersions = plugin.versions && plugin.versions.length > 0; + const submitButtonText = hasPublishedVersions + ? "Submit New Version" + : "Submit Plugin"; + + return ( +
+ + Back to Dashboard + + +
+
+
+ +
+
+

{plugin.displayName}

+
+ + {plugin.pluginType} + + + {plugin.status.replace(/_/g, " ")} + +
+
+
+ +
+ {hasPendingVersion ? ( + + ) : latestEligibleBuild ? ( + + {submitButtonText} + + ) : ( + + )} + + + Public Page + + +
+
+ +
+
+

+ Build History +

+ + {buildsWithPlugin.length > 0 ? ( + + ) : ( +
+

+ No builds found for this plugin. +

+

+ Push code to your repository to trigger a new build. +

+
+ )} +
+ +
+

+ Analytics & Status +

+ +
+
+
+ Visibility +
+
{plugin.status}
+
+ +
+
+
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 2a49e73..d9ad46c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -68,26 +68,40 @@ 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 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 } } + } + } +`; +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 (
{timeAgo(build.createdAt)} - {build.duration ? ` • ${build.duration}s` : ""} • {build.branch || "master"} + {build.duration ? ` • ${build.duration}s` : ""} •{" "} + {build.branch || "master"}
- - - - + + + + @@ -215,7 +216,7 @@ export function BuildsTableClient({ Date - Lint + Status Commit @@ -252,7 +253,7 @@ export function BuildsTableClient({ )}
- + {timeAgo(build.createdAt)} {build.duration ? ( diff --git a/src/app/plugins/[slug]/builds/page.tsx b/src/app/plugins/[slug]/builds/page.tsx index b70ebfc..5fc687c 100644 --- a/src/app/plugins/[slug]/builds/page.tsx +++ b/src/app/plugins/[slug]/builds/page.tsx @@ -24,9 +24,7 @@ export default async function PluginBuildsPage({ params: { slug: string }; }) { const [buildsRes, session] = await Promise.all([ - fetchApi(`/api/v1/builds/plugin/${params.slug}?page=1&pageSize=10`, { - noAuth: true, - }), + fetchApi(`/api/v1/builds/plugin/${params.slug}?page=1&pageSize=10`), getServerSession(authOptions), ]); @@ -43,7 +41,10 @@ export default async function PluginBuildsPage({ const sessionUsername = (session?.user as any)?.username; const pluginAuthorUsername = plugin.author?.username; - const isOwner = !!sessionUsername && !!pluginAuthorUsername && sessionUsername === pluginAuthorUsername; + const isOwner = + !!sessionUsername && + !!pluginAuthorUsername && + sessionUsername === pluginAuthorUsername; return ( import("@/components/DependencyGraph"), { - ssr: false, - loading: () => ( -
-

Loading dependency graph...

-
- ), -}); +const DependencyGraph = nextDynamic( + () => import("@/components/DependencyGraph"), + { + ssr: false, + loading: () => ( +
+

Loading dependency graph...

+
+ ), + }, +); const PluginDiscussion = nextDynamic( () => import("@/components/PluginDiscussion"), { @@ -55,20 +58,30 @@ const PluginDiscussion = nextDynamic( ), }, ); -const VirusTotalCard = nextDynamic(() => import("@/components/VirusTotalCard"), { - ssr: false, - loading: () => ( -
-

Loading security scan...

-
- ), -}); 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 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 } } + } + } +`; + 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({ @@ -147,7 +160,10 @@ export default async function PluginDetailPage({ const sessionUsername = (session?.user as any)?.username; const pluginAuthorUsername = plugin.author?.username; - const isAuthor = !!sessionUsername && !!pluginAuthorUsername && sessionUsername === pluginAuthorUsername; + const isAuthor = + !!sessionUsername && + !!pluginAuthorUsername && + sessionUsername === pluginAuthorUsername; const canEdit = isAuthor; const repoOwnerDetail = plugin.repoUrl?.match(/github\.com\/([^/]+)/)?.[1]; @@ -256,11 +272,14 @@ export default async function PluginDetailPage({ )} {/* Temp Debug Panel */} - +

by{" "} @@ -415,6 +434,80 @@ export default async function PluginDetailPage({

+ {/* Shield Markdown / HTML */} + {canEdit && ( +
+
+ Shield Markdown / HTML +
+
+
+
+ State +
+
+ {`[![State](https://endgit.dev/shield.state/${plugin.slug})](https://endgit.dev/plugins/${plugin.slug})`} +
+
+ {`State`} +
+
+
+
+
+ API +
+
+ {`[![API](https://endgit.dev/shield.api/${plugin.slug})](https://endgit.dev/plugins/${plugin.slug})`} +
+
+ {`API`} +
+
+
+
+
+ Downloads total +
+
+ {`[![Downloads total](https://endgit.dev/shield.dl.total/${plugin.slug})](https://endgit.dev/plugins/${plugin.slug})`} +
+
+ {`Downloads total`} +
+
+
+
+
+ Downloads +
+
+ {`[![Downloads](https://endgit.dev/shield.dl/${plugin.slug})](https://endgit.dev/plugins/${plugin.slug})`} +
+
+ {`Downloads`} +
+
+
+
+ )} + {/* Analytics Chart */} @@ -426,9 +519,6 @@ export default async function PluginDetailPage({ {/* Right Sidebar */}
- {/* Badges for Markdown */} - {canEdit && ( -
-

Markdown Badges

-

- Show off your plugin stats in your README. -

-
-
- Downloads Badge -
-
- [![Downloads](https://endgit.dev/shield.dl.total/{plugin.slug} - )](https://endgit.dev/plugins/{plugin.slug}) -
-
- -
-
- Status Badge -
-
- [![Status](https://endgit.dev/shield.state/{plugin.slug} - )](https://endgit.dev/plugins/{plugin.slug}) -
-
- -
-
- Version Badge -
-
- [![Version](https://endgit.dev/shield.version/{plugin.slug} - )](https://endgit.dev/plugins/{plugin.slug}) -
-
-
- )} - {/* Dependency Graph */} diff --git a/src/app/shield.api/[slug]/route.ts b/src/app/shield.api/[slug]/route.ts new file mode 100644 index 0000000..9dcb292 --- /dev/null +++ b/src/app/shield.api/[slug]/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from "next/server"; +import { createBadgeSvg, fetchPluginData } from "@/lib/badge"; + +export async function GET( + request: Request, + { params }: { params: { slug: string } }, +) { + try { + const plugin = await fetchPluginData(params.slug); + if (!plugin) { + 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`; + + 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", + }, + }); + } catch (error) { + 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 b2799a5..c41a066 100644 --- a/src/app/shield.dl.total/[slug]/route.ts +++ b/src/app/shield.dl.total/[slug]/route.ts @@ -1,61 +1,23 @@ import { NextResponse } from "next/server"; - -function formatNumber(num: number): string { - if (num >= 1000000) return (num / 1000000).toFixed(1) + "M"; - if (num >= 1000) return (num / 1000).toFixed(1) + "k"; - return num.toString(); -} - -function createBadgeSvg(label: string, message: string, color: string): string { - // A simple flat badge SVG generator - const labelWidth = label.length * 7 + 10; - const messageWidth = message.length * 7 + 10; - const totalWidth = labelWidth + messageWidth; - - return ` - - - - - - - - - - - - - - ${label} - ${label} - ${message} - ${message} - - `; -} +import { createBadgeSvg, fetchPluginData } from "@/lib/badge"; export async function GET( request: Request, { params }: { params: { slug: string } }, ) { try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"; - const res = await fetch(`${apiUrl}/api/v1/plugins/${params.slug}`); - - if (!res.ok) { + const plugin = await fetchPluginData(params.slug); + if (!plugin) { return new NextResponse( - createBadgeSvg("downloads", "not found", "#e05d44"), - { - headers: { "Content-Type": "image/svg+xml" }, - }, + createBadgeSvg("endgit", "not found", "#e05d44"), + { headers: { "Content-Type": "image/svg+xml" } }, ); } - const { data: plugin } = await res.json(); - const downloads = plugin?.downloads || 0; + let dl = plugin.downloads || 0; return new NextResponse( - createBadgeSvg("downloads", formatNumber(downloads), "#007ec6"), + createBadgeSvg("endgit", `${dl} downloads total`, "#97ca00"), { headers: { "Content-Type": "image/svg+xml", @@ -64,7 +26,7 @@ export async function GET( }, ); } catch (error) { - return new NextResponse(createBadgeSvg("downloads", "error", "#e05d44"), { + 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 new file mode 100644 index 0000000..6409fbd --- /dev/null +++ b/src/app/shield.dl/[slug]/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from "next/server"; +import { createBadgeSvg, fetchPluginData } from "@/lib/badge"; + +export async function GET( + request: Request, + { params }: { params: { slug: string } }, +) { + try { + const plugin = await fetchPluginData(params.slug); + if (!plugin) { + 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`; + + 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", + }, + }, + ); + } catch (error) { + return new NextResponse(createBadgeSvg("error", "error", "#e05d44"), { + headers: { "Content-Type": "image/svg+xml" }, + }); + } +} diff --git a/src/app/shield.rating/[slug]/route.ts b/src/app/shield.rating/[slug]/route.ts deleted file mode 100644 index cf68e5f..0000000 --- a/src/app/shield.rating/[slug]/route.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { NextResponse } from "next/server"; - -function createBadgeSvg(label: string, message: string, color: string): string { - const labelWidth = label.length * 7 + 10; - const messageWidth = message.length * 7 + 10; - const totalWidth = labelWidth + messageWidth; - - return ` - - - - - - - - - - - - - - ${label} - ${label} - ${message} - ${message} - - `; -} - -export async function GET( - request: Request, - { params }: { params: { slug: string } }, -) { - try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"; - const res = await fetch(`${apiUrl}/api/v1/plugins/${params.slug}`); - - if (!res.ok) { - return new NextResponse( - createBadgeSvg("rating", "not found", "#e05d44"), - { - headers: { "Content-Type": "image/svg+xml" }, - }, - ); - } - - const { data: plugin } = await res.json(); - const avgRating = plugin.stars - ? Math.round((plugin.stars / 20) * 10) / 10 - : 0; - - let color = "#9f9f9f"; // lightgrey - if (avgRating >= 4.5) - color = "#4c1"; // brightgreen - else if (avgRating >= 3.5) - color = "#97ca00"; // green - else if (avgRating >= 2.5) - color = "#dfb317"; // yellow - else if (avgRating > 0) color = "#fe7d37"; // orange - - const message = avgRating > 0 ? `${avgRating}/5` : "unrated"; - - return new NextResponse(createBadgeSvg("rating", message, color), { - headers: { - "Content-Type": "image/svg+xml", - "Cache-Control": "public, max-age=3600", - }, - }); - } catch (error) { - return new NextResponse(createBadgeSvg("rating", "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 07aca7e..4096922 100644 --- a/src/app/shield.state/[slug]/route.ts +++ b/src/app/shield.state/[slug]/route.ts @@ -1,72 +1,48 @@ import { NextResponse } from "next/server"; - -function createBadgeSvg(label: string, message: string, color: string): string { - // A simple flat badge SVG generator - const labelWidth = label.length * 7 + 10; - const messageWidth = message.length * 7 + 10; - const totalWidth = labelWidth + messageWidth; - - return ` - - - - - - - - - - - - - - ${label} - ${label} - ${message} - ${message} - - `; -} +import { createBadgeSvg, fetchPluginData } from "@/lib/badge"; export async function GET( request: Request, { params }: { params: { slug: string } }, ) { try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"; - const res = await fetch(`${apiUrl}/api/v1/plugins/${params.slug}`); - - if (!res.ok) { + const plugin = await fetchPluginData(params.slug); + if (!plugin) { return new NextResponse( - createBadgeSvg("status", "not found", "#e05d44"), - { - headers: { "Content-Type": "image/svg+xml" }, - }, + createBadgeSvg("unknown @endgit", "not found", "#e05d44"), + { headers: { "Content-Type": "image/svg+xml" } }, ); } - const { data: plugin } = await res.json(); - const status = plugin?.status || "unknown"; + 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 for unknown - let message = status.toLowerCase(); + let color = "#9ca3af"; // gray + let message = status?.toLowerCase() || "unknown"; if (status === "APPROVED") { - color = "#10b981"; // green + color = "#4c1"; // green + message = "Approved"; } else if (status === "PENDING") { - color = "#f59e0b"; // yellow + color = "#dfb317"; // yellow + message = "Pending"; } else if (status === "REJECTED") { - color = "#ef4444"; // red + color = "#e05d44"; // red + message = "Rejected"; } - return new NextResponse(createBadgeSvg("status", message, color), { + return new NextResponse(createBadgeSvg(versionLabel, message, color), { headers: { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=3600", }, }); } catch (error) { - return new NextResponse(createBadgeSvg("status", "error", "#e05d44"), { + return new NextResponse(createBadgeSvg("error", "error", "#e05d44"), { headers: { "Content-Type": "image/svg+xml" }, }); } diff --git a/src/app/shield.version/[slug]/route.ts b/src/app/shield.version/[slug]/route.ts deleted file mode 100644 index 2ae2786..0000000 --- a/src/app/shield.version/[slug]/route.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { NextResponse } from "next/server"; - -function createBadgeSvg(label: string, message: string, color: string): string { - const labelWidth = label.length * 7 + 10; - const messageWidth = message.length * 7 + 10; - const totalWidth = labelWidth + messageWidth; - - return ` - - - - - - - - - - - - - - ${label} - ${label} - ${message} - ${message} - - `; -} - -export async function GET( - request: Request, - { params }: { params: { slug: string } }, -) { - try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"; - // We only need the latest approved version. The plugin API returns 'versions' if it's the detail endpoint - const res = await fetch(`${apiUrl}/api/v1/plugins/${params.slug}`); - - if (!res.ok) { - return new NextResponse( - createBadgeSvg("version", "not found", "#e05d44"), - { - headers: { "Content-Type": "image/svg+xml" }, - }, - ); - } - - const { data: plugin } = await res.json(); - let versionStr = "unknown"; - - if (plugin.versions && plugin.versions.length > 0) { - const approvedVersion = plugin.versions.find( - (v: any) => v.status === "APPROVED", - ); - if (approvedVersion) { - versionStr = approvedVersion.version; - } - } - - const color = versionStr === "unknown" ? "#9f9f9f" : "#4c1"; // brightgreen - - return new NextResponse(createBadgeSvg("version", versionStr, color), { - headers: { - "Content-Type": "image/svg+xml", - "Cache-Control": "public, max-age=3600", - }, - }); - } catch (error) { - return new NextResponse(createBadgeSvg("version", "error", "#e05d44"), { - headers: { "Content-Type": "image/svg+xml" }, - }); - } -} diff --git a/src/components/BuildList.tsx b/src/components/BuildList.tsx index 29a426b..1c82b5f 100644 --- a/src/components/BuildList.tsx +++ b/src/components/BuildList.tsx @@ -86,9 +86,21 @@ function statusBorderClass(status: string) { export default function BuildsList({ builds, today, + filterByToday = true, + hideHeader = false, + showPluginName = true, + scrollableWrapper = false, + hideWarning = false, + linkToBuildDetail = false, }: { builds: any[]; today: string; + filterByToday?: boolean; + hideHeader?: boolean; + showPluginName?: boolean; + scrollableWrapper?: boolean; + hideWarning?: boolean; + linkToBuildDetail?: boolean; }) { const [query, setQuery] = useState(""); @@ -98,21 +110,24 @@ export default function BuildsList({ return builds.filter((build) => { const plugin = build.plugin?.displayName || build.plugin?.name || ""; const author = build.plugin?.author?.username || ""; + const commitMsg = build.commitMessage || ""; const matchesSearch = plugin.toLowerCase().includes(q) || author.toLowerCase().includes(q) || + commitMsg.toLowerCase().includes(q) || build.branch?.toLowerCase().includes(q) || - build.status?.toLowerCase().includes(q); + build.status?.toLowerCase().includes(q) || + String(build.buildNumber).includes(q); - if (!q) { - // If no search query, only show today's builds + if (!q && filterByToday) { + // If no search query and filtering by today, only show today's builds const isToday = new Date(build.createdAt).toISOString().slice(0, 10) === today; return isToday && matchesSearch; } - // If there is a search query, show any matching build regardless of date + // If there is a search query or filterByToday is false, show matching builds return matchesSearch; }); }, [builds, query, today]); @@ -120,23 +135,25 @@ export default function BuildsList({ return ( <> {/* Header */} -
-
-

Dev Builds

- - - Today ({today}) - + {!hideHeader && ( +
+
+

Dev Builds

+ + + Today ({today}) + +
+ +

+ CI builds from developer pushes today (UTC). +

- -

- CI builds from developer pushes today (UTC). -

-
+ )} {/* Search */}
-
+
{/* Warning */} -
- - -
- Caution: Development builds may - be unstable. + {!hideWarning && ( +
+ + +
+ Caution: Development builds + may be unstable. +
-
+ )} {/* Builds */} {filteredBuilds.length === 0 ? ( @@ -167,44 +186,58 @@ export default function BuildsList({

No matching builds

) : ( -
- {filteredBuilds.map((build: any, i: number) => ( - -
-
-

- {build.plugin?.displayName || build.plugin?.name} -

-
- - {build.plugin?.author?.username || "unknown"} - - - - {build.branch} - - #{build.buildNumber} +
+
+ {filteredBuilds.map((build: any, i: number) => ( + +
+
+

+ {showPluginName + ? build.plugin?.displayName || build.plugin?.name + : build.commitMessage || "Manual/Webhook Build"} +

+
+ + {build.plugin?.author?.username || "unknown"} + + + + {build.branch} + + #{build.buildNumber} +
-
-
- - - {build.status} - +
+ + + {build.status} + +
-
- - ))} + + ))} +
)} 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/LiveBuildLog.tsx b/src/components/LiveBuildLog.tsx index 53b4536..245905e 100644 --- a/src/components/LiveBuildLog.tsx +++ b/src/components/LiveBuildLog.tsx @@ -100,13 +100,14 @@ export default function LiveBuildLog({ return (
- + {index + 1 + lineOffset} {line} @@ -115,7 +116,7 @@ export default function LiveBuildLog({ }; return ( -
+
{/* Header */}
@@ -154,7 +155,7 @@ export default function LiveBuildLog({ setAutoScroll(scrollHeight - scrollTop - clientHeight < 50); } }} - className="m-0 py-4 font-mono text-[0.8125rem] max-h-[500px] overflow-y-auto scrollbar-terminal" + className="m-0 py-4 font-mono text-[0.8125rem] max-h-[500px] overflow-y-auto overflow-x-auto scrollbar-terminal" > {isTruncated && (
@@ -169,21 +170,23 @@ export default function LiveBuildLog({ {logs ? ( visibleLines.map((line, i) => parseLogLine(line, i)) ) : ( -
- +
+ 1 - + Waiting for build agent to start...
)} {isRunning && ( -
- +
+ {logs ? allLines.length + 1 : 2} - + + ▋ +
)}
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" /> {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[] }) {
diff --git a/src/components/VersionSelector.tsx b/src/components/VersionSelector.tsx index a678b9f..a778808 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)}
); diff --git a/src/lib/api.ts b/src/lib/api.ts index dbe87e5..eca7412 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -54,3 +54,25 @@ 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 }; +} diff --git a/src/lib/badge.ts b/src/lib/badge.ts new file mode 100644 index 0000000..6c15d5a --- /dev/null +++ b/src/lib/badge.ts @@ -0,0 +1,58 @@ +import { fetchGraphQL } from "@/lib/api"; + +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; + + const labelX = Math.round((labelWidth / 2) * 10); + const messageX = Math.round((labelWidth + messageWidth / 2) * 10); + + return ` + ${label}: ${message} + + + + + + + + + + + + + + + ${label} + + ${message} + + `; +} + +export async function fetchPluginData(slug: string) { + try { + const { data } = await fetchGraphQL( + ` + query GetPlugin($slug: String!) { + plugin(slug: $slug) { + status + downloads + versions { version status downloads supportedApis isLatest } + } + } + `, + { slug }, + { noAuth: true }, + ); + return data?.plugin || null; + } catch (error) { + console.error("fetchPluginData error:", error); + return null; + } +}