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
+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"]
}