feat: portal SAM - Next.js 15 + Bootstrap, integración con deployer API
This commit is contained in:
1 parent
229cbd074a
commit
7691c01ff1
9 files changed
+1005
-107
No files matched your search
+24
@@ -0,0 +1,24 @@
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
FROM base AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
CMD ["node", "server.js"]
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
Generated
+262
-5
@@ -8,6 +8,8 @@
|
||||
"name": "sam-portal",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.8",
|
||||
"boxicons": "^2.1.4",
|
||||
"next": "16.3.4",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8"
|
||||
@@ -228,6 +230,15 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||
@@ -1377,6 +1388,17 @@
|
||||
"node": ">=12.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@rtsao/scc": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||
@@ -2387,6 +2409,12 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@webcomponents/webcomponentsjs": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.8.0.tgz",
|
||||
"integrity": "sha512-loGD63sacRzOzSJgQnB9ZAhaQGkN7wl2Zuw7tsphI5Isa0irijrRo6EnJii/GgjGefIFO8AIO7UivzRhFaEk9w==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
|
||||
@@ -2692,6 +2720,78 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "5.3.8",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
|
||||
"integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/twbs"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@popperjs/core": "^2.11.8"
|
||||
}
|
||||
},
|
||||
"node_modules/boxicons": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/boxicons/-/boxicons-2.1.4.tgz",
|
||||
"integrity": "sha512-BvJNfYfnE4g9WQ7GL91fftxMOTwAleWlPFwvQJPYb/Ju7aLjlQ/Eu55AH9JLNk/OR82z+ZSq4TbKzbV/e5Rr0A==",
|
||||
"license": "(CC-BY-4.0 OR OFL-1.1 OR MIT)",
|
||||
"dependencies": {
|
||||
"@webcomponents/webcomponentsjs": "^2.0.2",
|
||||
"prop-types": "^15.6.0",
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-interactive": "^0.8.1",
|
||||
"react-router-dom": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/boxicons/node_modules/react": {
|
||||
"version": "16.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
|
||||
"integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/boxicons/node_modules/react-dom": {
|
||||
"version": "16.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz",
|
||||
"integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.19.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/boxicons/node_modules/scheduler": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",
|
||||
"integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
@@ -3031,6 +3131,24 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-hover": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-hover/-/detect-hover-1.0.3.tgz",
|
||||
"integrity": "sha512-HtLoY+tClgYucJNiovNICGWFp9nOGVmHY44s7L62iPqORXM9vujeWFaVcqtA7XRvp/2Y+4RBUfHbDKFGN+xxZQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-it": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/detect-it/-/detect-it-3.0.7.tgz",
|
||||
"integrity": "sha512-RxpgcdbatUX6epJE09K16iJqF7x6iEcEdoL18FR2zpBO4JhnL7aMOAUoUEyexdtbWOSfTmoDWmeD6mwRBQyRXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-hover": "^1.0.3",
|
||||
"detect-passive-events": "^1.0.5",
|
||||
"detect-pointer": "^1.0.3",
|
||||
"detect-touch-events": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -3041,6 +3159,24 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-passive-events": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/detect-passive-events/-/detect-passive-events-1.0.5.tgz",
|
||||
"integrity": "sha512-foW7Q35wwOCxVzW0xLf5XeB5Fhe7oyRgvkBYdiP9IWgLMzjqUqTvsJv9ymuEWGjY6AoDXD3OC294+Z9iuOw0QA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-pointer": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-pointer/-/detect-pointer-1.0.3.tgz",
|
||||
"integrity": "sha512-d0o/Puo3fiGSCXy6H039h9Kwz+mmYCGKZ/qtPFnpN3WfsumjC1C9b5KKvRu+aYnfdI8peqN/iAe7dPd85qIt2g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-touch-events": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-touch-events/-/detect-touch-events-2.0.2.tgz",
|
||||
"integrity": "sha512-g8GWBkJLiIDRJfRXEdrd1wMXpNyGId2DkbfuwFahSb4OCvn717hyRJtAcEDISfp3zkwEhZ4Y4woHPA6DeyB3Fw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
@@ -4175,6 +4311,26 @@
|
||||
"hermes-estree": "0.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/history": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"loose-envify": "^1.2.0",
|
||||
"resolve-pathname": "^3.0.0",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0",
|
||||
"value-equal": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/hoist-non-react-statics": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz",
|
||||
"integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -4227,6 +4383,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-array-buffer": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||
@@ -4704,7 +4869,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
@@ -5137,7 +5301,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
@@ -5385,7 +5548,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -5613,6 +5775,21 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
|
||||
"integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"isarray": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp/node_modules/isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -5685,7 +5862,6 @@
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
@@ -5745,13 +5921,61 @@
|
||||
"react": "^19.2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-interactive": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/react-interactive/-/react-interactive-0.8.3.tgz",
|
||||
"integrity": "sha512-mmRvA9aKP7zu9kVfP1AX1egX8tFlnE3DDXq92z0JTZezfOpmeQBzr77O1+mTV54OOmn+M2t6c5kFD5VnuFoM7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-it": "^3.0.3",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=15"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz",
|
||||
"integrity": "sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"history": "^4.7.2",
|
||||
"hoist-non-react-statics": "^2.5.0",
|
||||
"invariant": "^2.2.4",
|
||||
"loose-envify": "^1.3.1",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
"prop-types": "^15.6.1",
|
||||
"warning": "^4.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=15"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.3.1.tgz",
|
||||
"integrity": "sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"history": "^4.7.2",
|
||||
"invariant": "^2.2.4",
|
||||
"loose-envify": "^1.3.1",
|
||||
"prop-types": "^15.6.1",
|
||||
"react-router": "^4.3.1",
|
||||
"warning": "^4.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=15"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||
@@ -5830,6 +6054,12 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pathname": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
|
||||
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
@@ -6394,6 +6624,18 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-warning": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
@@ -6734,6 +6976,21 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/value-equal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
|
||||
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.8",
|
||||
"boxicons": "^2.1.4",
|
||||
"next": "16.3.4",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8"
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const DEPLOYER_URL = process.env.DEPLOYER_URL || "http://127.0.0.1:3010";
|
||||
const DEPLOYER_TOKEN = process.env.DEPLOYER_TOKEN || "";
|
||||
|
||||
function headers() {
|
||||
const h: Record<string, string> = { "Content-Type": "application/json" };
|
||||
if (DEPLOYER_TOKEN) h["X-API-Token"] = DEPLOYER_TOKEN;
|
||||
return h;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const res = await fetch(`${DEPLOYER_URL}/api/modules`, {
|
||||
headers: headers(),
|
||||
cache: "no-store",
|
||||
});
|
||||
const data = await res.json();
|
||||
return NextResponse.json(data, { status: res.status });
|
||||
} catch (e) {
|
||||
return NextResponse.json(
|
||||
{ error: e instanceof Error ? e.message : "Error al conectar con deployer" },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const res = await fetch(`${DEPLOYER_URL}/api/modules`, {
|
||||
method: "POST",
|
||||
headers: headers(),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json();
|
||||
return NextResponse.json(data, { status: res.status });
|
||||
} catch (e) {
|
||||
return NextResponse.json(
|
||||
{ error: e instanceof Error ? e.message : "Error al registrar módulo" },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
+141
-21
@@ -1,26 +1,146 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
--primary: #00285d;
|
||||
--primary-light: #1a3f7a;
|
||||
--primary-gradient: linear-gradient(135deg, #00285d 0%, #1a4a8a 100%);
|
||||
--card-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
--card-shadow-hover: 0 6px 24px rgba(0, 0, 0, 0.14);
|
||||
--radius: 12px;
|
||||
--transition: 0.25s ease;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-family: "Fira Sans", sans-serif;
|
||||
background-color: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidenav {
|
||||
background: var(--primary-gradient);
|
||||
overflow-x: hidden;
|
||||
padding-top: 20px;
|
||||
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.login-main-text {
|
||||
padding: 60px 40px;
|
||||
}
|
||||
|
||||
.login-main-text img {
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.sidenav {
|
||||
padding: 20px 0 10px;
|
||||
}
|
||||
.login-main-text {
|
||||
padding: 20px;
|
||||
}
|
||||
.login-main-text h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.main {
|
||||
padding: 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.main {
|
||||
margin-left: 40%;
|
||||
}
|
||||
.sidenav {
|
||||
width: 40%;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
border-radius: var(--radius);
|
||||
border-left: 4px solid var(--primary);
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.app-card {
|
||||
background: #fff;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: transform var(--transition), box-shadow var(--transition),
|
||||
background-color var(--transition);
|
||||
border: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.app-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: var(--card-shadow-hover);
|
||||
background-color: #f8faff;
|
||||
}
|
||||
|
||||
.app-card a {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stretched-link ~ * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.app-card table {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.app-card-sm table {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.app-card .bx-border {
|
||||
border-radius: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.badge-respaldo {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
vertical-align: middle;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.row-cards > [class*="col-"] {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-healthy {
|
||||
background: #198754;
|
||||
box-shadow: 0 0 6px rgba(25, 135, 84, 0.5);
|
||||
}
|
||||
|
||||
.status-unhealthy {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.status-starting {
|
||||
background: #ffc107;
|
||||
}
|
||||
+20
-19
@@ -1,29 +1,30 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "SAM | ICHN - Portal de Módulos",
|
||||
description: "Sistema de Administración de Matrículas - Portal de integración de módulos",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<link
|
||||
href="https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Fira+Sans:400,500,600,700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
+280
-61
@@ -1,69 +1,288 @@
|
||||
import Image from "next/image";
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface ModuleState {
|
||||
name: string;
|
||||
repoUrl: string;
|
||||
branch: string;
|
||||
subdomain: string;
|
||||
port: number;
|
||||
active: boolean;
|
||||
lastDeploy?: {
|
||||
sha: string;
|
||||
time: string;
|
||||
ok: boolean;
|
||||
log: string;
|
||||
};
|
||||
container?: string | null;
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
backend: {
|
||||
repoUrl: string;
|
||||
branch: string;
|
||||
container: string | null;
|
||||
lastDeploy: ModuleState["lastDeploy"] | null;
|
||||
};
|
||||
modules: ModuleState[];
|
||||
}
|
||||
|
||||
const iconMap: Record<string, { icon: string; bg: string }> = {
|
||||
ventas: { icon: "bx-money", bg: "bg-success" },
|
||||
comercial: { icon: "bx-money", bg: "bg-success" },
|
||||
finanzas: { icon: "bx-detail", bg: "bg-danger" },
|
||||
contabilidad: { icon: "bx-detail", bg: "bg-danger" },
|
||||
academico: { icon: "bx-user", bg: "bg-primary" },
|
||||
"academico-empresas": { icon: "bx-briefcase", bg: "bg-primary" },
|
||||
profesor: { icon: "bx-pencil", bg: "bg-warning text-dark" },
|
||||
profesoras: { icon: "bx-pencil", bg: "bg-warning text-dark" },
|
||||
empresas: { icon: "bx-building", bg: "bg-info" },
|
||||
pagos: { icon: "bx-credit-card", bg: "bg-success" },
|
||||
default: { icon: "bx-cube", bg: "bg-secondary" },
|
||||
};
|
||||
|
||||
function getModuleMeta(name: string) {
|
||||
const key = name.toLowerCase();
|
||||
return iconMap[key] || iconMap.default;
|
||||
}
|
||||
|
||||
function getStatusInfo(m: ModuleState) {
|
||||
if (!m.container) return { label: "Detenido", cls: "status-unhealthy" };
|
||||
if (m.container.includes("healthy")) return { label: "Activo", cls: "status-healthy" };
|
||||
if (m.container.includes("starting")) return { label: "Iniciando", cls: "status-starting" };
|
||||
return { label: "Activo", cls: "status-healthy" };
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [data, setData] = useState<ApiResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchModules = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await fetch("/api/modules", { cache: "no-store" });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const json = await res.json();
|
||||
setData(json);
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Error al cargar módulos");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchModules();
|
||||
const id = setInterval(fetchModules, 15000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const modules = data?.modules ?? [];
|
||||
const backend = data?.backend;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
<Image
|
||||
className="dark:invert h-5 w-[100px]"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the{" "}
|
||||
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
|
||||
page.tsx
|
||||
</code>{" "}
|
||||
file.
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
<>
|
||||
<div className="sidenav">
|
||||
<div className="row text-light login-main-text">
|
||||
<div className="col-12 text-center">
|
||||
<div
|
||||
className="mx-auto d-flex align-items-center justify-content-center bg-white rounded-3"
|
||||
style={{ width: 80, height: 80 }}
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
<i className="bx bx-layer text-primary" style={{ fontSize: 48 }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 text-center mt-3">
|
||||
<h1 className="fw-bold fs-2">Sistema SAM</h1>
|
||||
</div>
|
||||
<div className="col-12 text-center">
|
||||
<h4 className="fw-semibold opacity-75">Aplicaciones</h4>
|
||||
</div>
|
||||
<div className="col-12 text-center mt-4">
|
||||
<Link href="/upload" className="btn btn-light btn-sm px-4">
|
||||
<i className="bx bx-plus me-1" /> Nuevo Módulo
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-12 text-center mt-3">
|
||||
<small className="opacity-50">Portal de integración</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert h-[14px] w-4"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={14}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="main">
|
||||
<div className="row" id="modulos">
|
||||
<div className="col-lg-1" />
|
||||
<div className="col-lg-10 mt-4">
|
||||
{loading && (
|
||||
<div className="text-center py-5">
|
||||
<div className="spinner-border text-primary" role="status" />
|
||||
<p className="mt-2 text-muted">Cargando módulos...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-danger d-flex align-items-center">
|
||||
<i className="bx bx-error-circle me-2 fs-5" />
|
||||
<div>
|
||||
<strong>Error:</strong> {error}
|
||||
<button
|
||||
onClick={fetchModules}
|
||||
className="btn btn-sm btn-outline-danger ms-3"
|
||||
>
|
||||
Reintentar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && (
|
||||
<div className="row row-cards">
|
||||
<div className="col-12">
|
||||
<div className="alert alert-light section-header mb-3 py-2 px-3 d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<i className="bx bx-grid-alt me-1" /> Selección de Aplicación
|
||||
</span>
|
||||
<span className="badge bg-primary rounded-pill">
|
||||
{modules.length} módulo{modules.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{modules.length === 0 ? (
|
||||
<div className="col-12">
|
||||
<div className="app-card p-4 text-center">
|
||||
<i className="bx bx-inbox fs-1 text-muted" />
|
||||
<p className="text-muted mt-2 mb-3">
|
||||
No hay módulos integrados. ¡Sube tu primer proyecto!
|
||||
</p>
|
||||
<Link href="/upload" className="btn btn-primary btn-sm">
|
||||
<i className="bx bx-upload me-1" /> Subir Proyecto
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
modules.map((m) => {
|
||||
const meta = getModuleMeta(m.name);
|
||||
const status = getStatusInfo(m);
|
||||
const url = `https://${m.subdomain}.devops.norteamericano.cl`;
|
||||
const displayName =
|
||||
m.name.charAt(0).toUpperCase() + m.name.slice(1).replace(/-/g, " ");
|
||||
return (
|
||||
<div className="col-md-6" key={m.name}>
|
||||
<div className="app-card p-2">
|
||||
<table className="table table-borderless mb-0 align-middle">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="ps-3" style={{ width: 56 }}>
|
||||
<i
|
||||
className={`bx ${meta.icon} bx-sm bx-border text-light ${meta.bg}`}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-decoration-none text-dark d-block py-2 stretched-link"
|
||||
>
|
||||
{displayName}
|
||||
</a>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<span className={`status-dot ${status.cls}`} />
|
||||
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
|
||||
{status.label} · :{m.port} · {m.branch}
|
||||
</small>
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-end pe-3" style={{ width: 40 }}>
|
||||
<i className="bx bx-chevron-right text-muted" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
{backend && (
|
||||
<>
|
||||
<div className="col-12 mt-4 mb-2">
|
||||
<div className="alert alert-light section-header mb-3 py-2 px-3">
|
||||
<i className="bx bx-server me-1" /> Backend & Sistema
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="app-card p-2">
|
||||
<table className="table table-borderless mb-0 align-middle">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="ps-3" style={{ width: 56 }}>
|
||||
<i className="bx bx-code-alt bx-sm bx-border text-light bg-dark" />
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
href="https://api.devops.norteamericano.cl/health"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-decoration-none text-dark d-block py-2 stretched-link"
|
||||
>
|
||||
API Backend
|
||||
</a>
|
||||
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
|
||||
{backend.container ? "Activo" : "Detenido"} · {backend.branch}
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="app-card p-2">
|
||||
<table className="table table-borderless mb-0 align-middle">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="ps-3" style={{ width: 56 }}>
|
||||
<i className="bx bx-cog bx-sm bx-border text-light bg-secondary" />
|
||||
</td>
|
||||
<td>
|
||||
<Link
|
||||
href="/upload"
|
||||
className="text-decoration-none text-dark d-block py-2 stretched-link"
|
||||
>
|
||||
Gestionar Módulos
|
||||
</Link>
|
||||
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
|
||||
Subir / actualizar proyectos
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="col-12 mt-4 mb-4 text-center">
|
||||
<small className="text-muted">
|
||||
Auto-actualizado cada 15s ·{" "}
|
||||
<a href="#" onClick={(e) => { e.preventDefault(); fetchModules(); }}>
|
||||
actualizar ahora
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function UploadPage() {
|
||||
const [form, setForm] = useState({
|
||||
name: "",
|
||||
repoUrl: "",
|
||||
branch: "main",
|
||||
port: "",
|
||||
subdomain: "",
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState<{ ok: boolean; message: string } | null>(null);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setForm({ ...form, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setResult(null);
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
name: form.name.trim().toLowerCase(),
|
||||
repoUrl: form.repoUrl.trim(),
|
||||
branch: form.branch.trim() || "main",
|
||||
};
|
||||
if (form.port) payload.port = parseInt(form.port, 10);
|
||||
if (form.subdomain) payload.subdomain = form.subdomain.trim();
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/modules", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setResult({
|
||||
ok: true,
|
||||
message: `Módulo "${payload.name}" registrado. Deploy: ${data.webhook ? "webhook creado" : "sin webhook"}${data.deployLog ? " · " + data.deployLog.slice(0, 80) : ""}`,
|
||||
});
|
||||
setForm({ name: "", repoUrl: "", branch: "main", port: "", subdomain: "" });
|
||||
} else {
|
||||
setResult({ ok: false, message: data.error || `Error HTTP ${res.status}` });
|
||||
}
|
||||
} catch (err) {
|
||||
setResult({ ok: false, message: err instanceof Error ? err.message : "Error de red" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sidenav">
|
||||
<div className="row text-light login-main-text">
|
||||
<div className="col-12 text-center">
|
||||
<div
|
||||
className="mx-auto d-flex align-items-center justify-content-center bg-white rounded-3"
|
||||
style={{ width: 80, height: 80 }}
|
||||
>
|
||||
<i className="bx bx-upload text-primary" style={{ fontSize: 48 }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 text-center mt-3">
|
||||
<h1 className="fw-bold fs-2">Sistema SAM</h1>
|
||||
</div>
|
||||
<div className="col-12 text-center">
|
||||
<h4 className="fw-semibold opacity-75">Subir Proyecto</h4>
|
||||
</div>
|
||||
<div className="col-12 text-center mt-4">
|
||||
<Link href="/" className="btn btn-outline-light btn-sm px-4">
|
||||
<i className="bx bx-arrow-back me-1" /> Volver
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="main">
|
||||
<div className="row">
|
||||
<div className="col-lg-1" />
|
||||
<div className="col-lg-10 mt-4">
|
||||
<div className="alert alert-light section-header mb-3 py-2 px-3">
|
||||
<i className="bx bx-cloud-upload me-1" /> Integrar Nuevo Módulo
|
||||
</div>
|
||||
|
||||
<div className="app-card p-4">
|
||||
<p className="text-muted mb-4">
|
||||
Registra un proyecto <strong>Next.js 15 + Bootstrap</strong> desde un repositorio
|
||||
Gitea. Se clonará, desplegará automáticamente y quedará disponible en{" "}
|
||||
<code>https://{"{subdominio}"}.devops.norteamericano.cl</code>. Cada push futuro
|
||||
actualiza el módulo vía webhook.
|
||||
</p>
|
||||
|
||||
{result && (
|
||||
<div className={`alert ${result.ok ? "alert-success" : "alert-danger"}`}>
|
||||
<i className={`bx ${result.ok ? "bx-check-circle" : "bx-error-circle"} me-2`} />
|
||||
{result.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="row g-3">
|
||||
<div className="col-md-6">
|
||||
<label className="form-label fw-semibold">
|
||||
Nombre del módulo <span className="text-danger">*</span>
|
||||
</label>
|
||||
<input
|
||||
name="name"
|
||||
value={form.name}
|
||||
onChange={handleChange}
|
||||
placeholder="ej: ventas"
|
||||
pattern="^[a-z][a-z0-9-]+$"
|
||||
required
|
||||
className="form-control"
|
||||
/>
|
||||
<div className="form-text">Minúsculas, guiones, empieza con letra.</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<label className="form-label fw-semibold">
|
||||
Subdominio <small className="text-muted">(opcional)</small>
|
||||
</label>
|
||||
<input
|
||||
name="subdomain"
|
||||
value={form.subdomain}
|
||||
onChange={handleChange}
|
||||
placeholder="ej: ventas (default: nombre)"
|
||||
className="form-control"
|
||||
/>
|
||||
<div className="form-text">https://{form.subdomain || form.name || "subdominio"}.devops.norteamericano.cl</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<label className="form-label fw-semibold">
|
||||
URL del repositorio Gitea <span className="text-danger">*</span>
|
||||
</label>
|
||||
<input
|
||||
name="repoUrl"
|
||||
value={form.repoUrl}
|
||||
onChange={handleChange}
|
||||
placeholder="https://git.norteamericano.cl/Fimm/mi-frontend.git"
|
||||
required
|
||||
className="form-control"
|
||||
/>
|
||||
<div className="form-text">Debe ser accesible desde el deployer (https://git.norteamericano.cl).</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<label className="form-label fw-semibold">Rama</label>
|
||||
<input
|
||||
name="branch"
|
||||
value={form.branch}
|
||||
onChange={handleChange}
|
||||
placeholder="main"
|
||||
className="form-control"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<label className="form-label fw-semibold">
|
||||
Puerto <small className="text-muted">(opcional)</small>
|
||||
</label>
|
||||
<input
|
||||
name="port"
|
||||
type="number"
|
||||
value={form.port}
|
||||
onChange={handleChange}
|
||||
placeholder="auto (3011+)"
|
||||
min={3000}
|
||||
max={4000}
|
||||
className="form-control"
|
||||
/>
|
||||
<div className="form-text">Se asigna automáticamente si se deja vacío.</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 mt-2">
|
||||
<div className="alert alert-info py-2 small mb-0">
|
||||
<i className="bx bx-info-circle me-1" />
|
||||
El repositorio debe ser un proyecto <strong>Next.js 15</strong> (con{" "}
|
||||
<code>package.json</code> en raíz o en <code>frontend/</code>) y usar{" "}
|
||||
<strong>Bootstrap</strong>. Si no trae <code>Dockerfile</code>, se genera uno automáticamente.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 mt-3 d-flex gap-2">
|
||||
<button type="submit" disabled={loading} className="btn btn-primary px-4">
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="spinner-border spinner-border-sm me-2" /> Desplegando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="bx bx-rocket me-1" /> Registrar y Desplegar
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<Link href="/" className="btn btn-outline-secondary">
|
||||
Cancelar
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="app-card p-3 mt-3">
|
||||
<h6 className="fw-semibold mb-2">
|
||||
<i className="bx bx-book-open me-1" /> Requisitos del repositorio
|
||||
</h6>
|
||||
<ul className="small text-muted mb-0">
|
||||
<li>
|
||||
Proyecto Next.js 15 con <code>next build</code> / <code>next start</code>
|
||||
</li>
|
||||
<li>Bootstrap 5 (via CDN o npm) + Boxicons opcional</li>
|
||||
<li>
|
||||
Variable <code>NEXT_PUBLIC_API_URL=https://api.devops.norteamericano.cl</code>{" "}
|
||||
para consumir el backend
|
||||
</li>
|
||||
<li>Branch por defecto: main (configurable)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user