diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ea33044 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/next.config.ts b/next.config.ts index e9ffa30..68a6c64 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: "standalone", }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 04e7a92..d4f1a32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index f20fc9f..1a8084a 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/app/api/modules/route.ts b/src/app/api/modules/route.ts new file mode 100644 index 0000000..e5f59f3 --- /dev/null +++ b/src/app/api/modules/route.ts @@ -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 = { "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 } + ); + } +} diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..c20a2df 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9852c15..12782d0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 ( - - {children} + + + + + + {children} ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index c887311..ece26c3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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 = { + 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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( -
-
- Next.js logo -
-

- To get started, edit the{" "} - - page.tsx - {" "} - file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - +

+
+
+
- Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

+ +
+
+
+

Sistema SAM

+
+
+

Aplicaciones

+
+
+ + Nuevo Módulo + +
+
+ Portal de integración +
- + +
+
+
+
+ {loading && ( +
+
+

Cargando módulos...

+
+ )} + + {error && ( +
+ +
+ Error: {error} + +
+
+ )} + + {!loading && !error && ( +
+
+
+ + Selección de Aplicación + + + {modules.length} módulo{modules.length !== 1 ? "s" : ""} + +
+
+ + {modules.length === 0 ? ( +
+
+ +

+ No hay módulos integrados. ¡Sube tu primer proyecto! +

+ + Subir Proyecto + +
+
+ ) : ( + 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 ( +
+
+ + + + + + + + +
+ + + + {displayName} + +
+ + + {status.label} · :{m.port} · {m.branch} + +
+
+ +
+
+
+ ); + }) + )} + + {backend && ( + <> +
+
+ Backend & Sistema +
+
+
+
+ + + + + + + +
+ + + + API Backend + + + {backend.container ? "Activo" : "Detenido"} · {backend.branch} + +
+
+
+
+
+ + + + + + + +
+ + + + Gestionar Módulos + + + Subir / actualizar proyectos + +
+
+
+ + )} + +
+ + Auto-actualizado cada 15s ·{" "} + { e.preventDefault(); fetchModules(); }}> + actualizar ahora + + +
+
+ )} +
-
-
+ + ); } diff --git a/src/app/upload/page.tsx b/src/app/upload/page.tsx new file mode 100644 index 0000000..f1f6b38 --- /dev/null +++ b/src/app/upload/page.tsx @@ -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) => { + setForm({ ...form, [e.target.name]: e.target.value }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setResult(null); + + const payload: Record = { + 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 ( + <> +
+
+
+
+ +
+
+
+

Sistema SAM

+
+
+

Subir Proyecto

+
+
+ + Volver + +
+
+
+ +
+
+
+
+
+ Integrar Nuevo Módulo +
+ +
+

+ Registra un proyecto Next.js 15 + Bootstrap desde un repositorio + Gitea. Se clonará, desplegará automáticamente y quedará disponible en{" "} + https://{"{subdominio}"}.devops.norteamericano.cl. Cada push futuro + actualiza el módulo vía webhook. +

+ + {result && ( +
+ + {result.message} +
+ )} + +
+
+
+ + +
Minúsculas, guiones, empieza con letra.
+
+ +
+ + +
https://{form.subdomain || form.name || "subdominio"}.devops.norteamericano.cl
+
+ +
+ + +
Debe ser accesible desde el deployer (https://git.norteamericano.cl).
+
+ +
+ + +
+ +
+ + +
Se asigna automáticamente si se deja vacío.
+
+ +
+
+ + El repositorio debe ser un proyecto Next.js 15 (con{" "} + package.json en raíz o en frontend/) y usar{" "} + Bootstrap. Si no trae Dockerfile, se genera uno automáticamente. +
+
+ +
+ + + Cancelar + +
+
+ +
+ +
+
+ Requisitos del repositorio +
+
    +
  • + Proyecto Next.js 15 con next build / next start +
  • +
  • Bootstrap 5 (via CDN o npm) + Boxicons opcional
  • +
  • + Variable NEXT_PUBLIC_API_URL=https://api.devops.norteamericano.cl{" "} + para consumir el backend +
  • +
  • Branch por defecto: main (configurable)
  • +
+
+
+
+
+ + ); +}