feat: initial commit - Moraworld Imports project

This commit is contained in:
Lizandro Guarnizo
2026-06-01 08:06:40 -05:00
commit 094ea9cf81
47 changed files with 12844 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
# Coolify / producción — API NestJS
FROM node:22-alpine AS base
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
WORKDIR /app
FROM base AS deps
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml* ./
COPY apps/api/package.json ./apps/api/
COPY packages/database/package.json ./packages/database/
RUN pnpm install --frozen-lockfile 2>/dev/null || pnpm install
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm --filter @moraworld/database generate
RUN pnpm --filter @moraworld/database build
RUN pnpm --filter @moraworld/api build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/apps/api/dist ./dist
COPY --from=builder /app/apps/api/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/packages/database/node_modules/.prisma ./node_modules/.prisma
EXPOSE 3001
CMD ["node", "dist/main.js"]
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
+28
View File
@@ -0,0 +1,28 @@
{
"name": "@moraworld/api",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "nest build",
"dev": "nest start --watch",
"start": "node dist/main",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix"
},
"dependencies": {
"@moraworld/database": "workspace:*",
"@nestjs/common": "^11.1.0",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.0",
"@nestjs/platform-express": "^11.1.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2"
},
"devDependencies": {
"@nestjs/cli": "^11.0.7",
"@nestjs/schematics": "^11.0.5",
"@types/express": "^5.0.1",
"@types/node": "^22.15.21",
"typescript": "^5.8.3"
}
}
+16
View File
@@ -0,0 +1,16 @@
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { HealthModule } from "./health/health.module";
import { PrismaModule } from "./prisma/prisma.module";
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ["../../.env", ".env"],
}),
PrismaModule,
HealthModule,
],
})
export class AppModule {}
+27
View File
@@ -0,0 +1,27 @@
import { Controller, Get } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
@Controller()
export class HealthController {
constructor(private readonly prisma: PrismaService) {}
@Get("health")
async health() {
let database: "ok" | "error" = "ok";
try {
await this.prisma.client.$queryRaw`SELECT 1`;
} catch {
database = "error";
}
return {
status: database === "ok" ? "ok" : "degraded",
service: "moraworld-api",
version: "0.1.0",
timestamp: new Date().toISOString(),
checks: {
database,
},
};
}
}
+7
View File
@@ -0,0 +1,7 @@
import { Module } from "@nestjs/common";
import { HealthController } from "./health.controller";
@Module({
controllers: [HealthController],
})
export class HealthModule {}
+24
View File
@@ -0,0 +1,24 @@
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const corsOrigins = (process.env.CORS_ORIGINS ?? "http://localhost:3000")
.split(",")
.map((o) => o.trim());
app.enableCors({
origin: corsOrigins,
credentials: true,
});
app.setGlobalPrefix("api");
const port = process.env.API_PORT ?? process.env.PORT ?? 3001;
await app.listen(port, "0.0.0.0");
console.log(`Moraworld API → http://localhost:${port}/api`);
}
bootstrap();
+9
View File
@@ -0,0 +1,9 @@
import { Global, Module } from "@nestjs/common";
import { PrismaService } from "./prisma.service";
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
+15
View File
@@ -0,0 +1,15 @@
import { Injectable, OnModuleDestroy, OnModuleInit } from "@nestjs/common";
import { prisma, PrismaClient } from "@moraworld/database";
@Injectable()
export class PrismaService implements OnModuleInit, OnModuleDestroy {
readonly client: PrismaClient = prisma;
async onModuleInit() {
await prisma.$connect();
}
async onModuleDestroy() {
await prisma.$disconnect();
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2022",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}
+28
View File
@@ -0,0 +1,28 @@
# Coolify / producción — Next.js standalone
FROM node:22-alpine AS base
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
WORKDIR /app
FROM base AS deps
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml* ./
COPY apps/web/package.json ./apps/web/
RUN pnpm install --frozen-lockfile 2>/dev/null || pnpm install
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ARG NEXT_PUBLIC_API_URL=http://localhost:3001
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN pnpm --filter @moraworld/web build
FROM node:22-alpine 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/apps/web/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "apps/web/server.js"]
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;
+22
View File
@@ -0,0 +1,22 @@
{
"name": "@moraworld/web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --port 3000",
"build": "next build",
"start": "next start --port 3000",
"lint": "next lint"
},
"dependencies": {
"next": "^15.3.2",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@types/node": "^22.15.21",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"typescript": "^5.8.3"
}
}
View File
+34
View File
@@ -0,0 +1,34 @@
:root {
--primary: #0057ff;
--primary-dark: #003fc7;
--accent: #ff6b00;
--dark: #0d1117;
--gray-900: #111827;
--gray-500: #6b7280;
--gray-200: #e5e7eb;
--gray-100: #f3f4f6;
--white: #ffffff;
--green: #10b981;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-inter), system-ui, sans-serif;
color: var(--gray-900);
background: var(--white);
line-height: 1.6;
}
a {
color: inherit;
text-decoration: none;
}
+26
View File
@@ -0,0 +1,26 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
});
export const metadata: Metadata = {
title: "Moraworld Imports — Casillero en EE.UU. para Ecuador",
description:
"Compra en Amazon, eBay y más. Tu casillero en New Jersey con envío y gestión aduanera a Ecuador.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="es">
<body className={inter.variable}>{children}</body>
</html>
);
}
+115
View File
@@ -0,0 +1,115 @@
async function getApiHealth() {
const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001";
try {
const res = await fetch(`${apiUrl}/api/health`, {
next: { revalidate: 10 },
});
if (!res.ok) return null;
return res.json();
} catch {
return null;
}
}
export default async function HomePage() {
const health = await getApiHealth();
return (
<main>
<header
style={{
borderBottom: "1px solid var(--gray-200)",
padding: "16px 24px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span style={{ fontWeight: 800, fontSize: "1.2rem", color: "var(--primary)" }}>
Moraworld<span style={{ color: "var(--accent)" }}>.</span>Imports
</span>
<span
style={{
fontSize: ".75rem",
fontWeight: 600,
padding: "4px 10px",
borderRadius: 999,
background: health?.status === "ok" ? "#D1FAE5" : "#FEF3C7",
color: health?.status === "ok" ? "#065F46" : "#92400E",
}}
>
API: {health?.status === "ok" ? "conectada" : "local / pendiente"}
</span>
</header>
<section
style={{
background: "linear-gradient(135deg, #0D1117 0%, #0D2150 100%)",
padding: "80px 24px",
color: "#fff",
}}
>
<div style={{ maxWidth: 1100, margin: "0 auto" }}>
<p
style={{
display: "inline-block",
background: "rgba(0,87,255,.15)",
border: "1px solid rgba(0,87,255,.3)",
color: "#7AADFF",
fontSize: ".8rem",
fontWeight: 600,
padding: "6px 14px",
borderRadius: 999,
marginBottom: 24,
}}
>
Fase 0 Fundación del software
</p>
<h1 style={{ fontSize: "clamp(2rem, 5vw, 3rem)", fontWeight: 800, lineHeight: 1.15, marginBottom: 16 }}>
Tu casillero en <em style={{ color: "var(--accent)", fontStyle: "normal" }}>New Jersey</em>
<br />
para recibir en Ecuador
</h1>
<p style={{ color: "#9CA3AF", maxWidth: 520, marginBottom: 32 }}>
Monorepo activo: Next.js + NestJS + PostgreSQL. Desarrollo local con Docker; despliegue en Coolify.
</p>
<div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
<a
href="/status"
style={{
background: "var(--primary)",
color: "#fff",
padding: "14px 28px",
borderRadius: 8,
fontWeight: 600,
}}
>
Ver estado del sistema
</a>
<span
style={{
border: "2px solid rgba(255,255,255,.2)",
color: "#9CA3AF",
padding: "12px 26px",
borderRadius: 8,
fontSize: ".9rem",
}}
>
Prototipos HTML en la raíz del repo
</span>
</div>
</div>
</section>
<section style={{ padding: "48px 24px", maxWidth: 1100, margin: "0 auto" }}>
<h2 style={{ fontSize: "1.25rem", fontWeight: 700, marginBottom: 16 }}>Próximos pasos (Fase 1)</h2>
<ul style={{ color: "var(--gray-500)", paddingLeft: 20, display: "flex", flexDirection: "column", gap: 8 }}>
<li>Registro e inicio de sesión con JWT + MFA</li>
<li>Asignación automática de Suite (EC-XXXXX)</li>
<li>Pre-alertas y registro de paquetes</li>
<li>Migración del portal desde portal-cliente.html</li>
</ul>
</section>
</main>
);
}
+56
View File
@@ -0,0 +1,56 @@
async function getHealth() {
const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001";
const res = await fetch(`${apiUrl}/api/health`, { cache: "no-store" });
if (!res.ok) throw new Error("API no disponible");
return res.json();
}
export default async function StatusPage() {
let health: Record<string, unknown> | null = null;
let error: string | null = null;
try {
health = await getHealth();
} catch (e) {
error = e instanceof Error ? e.message : "Error desconocido";
}
return (
<main style={{ padding: 48, maxWidth: 720, margin: "0 auto", fontFamily: "var(--font-inter), sans-serif" }}>
<a href="/" style={{ color: "var(--primary)", fontWeight: 600, fontSize: ".9rem" }}>
Volver
</a>
<h1 style={{ fontSize: "1.75rem", fontWeight: 800, margin: "24px 0 16px" }}>Estado del sistema</h1>
{error ? (
<pre
style={{
background: "#FEE2E2",
color: "#991B1B",
padding: 20,
borderRadius: 12,
overflow: "auto",
}}
>
{error}
{"\n\n"}
Asegúrate de tener corriendo:{"\n"}
1. pnpm docker:up{"\n"}
2. pnpm dev (api + web)
</pre>
) : (
<pre
style={{
background: "var(--gray-100)",
padding: 20,
borderRadius: 12,
overflow: "auto",
fontSize: ".85rem",
}}
>
{JSON.stringify(health, null, 2)}
</pre>
)}
</main>
);
}
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./src/*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}