From 449c02eadc87d6f7eafd9e042a29bf3ea4a7e262 Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Tue, 28 Apr 2026 05:15:38 +0000 Subject: [PATCH] feat: phase 3 redesign, game images, auth system, vm guides, service isolation - Redesign all internal pages to warm/gold aesthetic (catalog, game detail, documentary, about, donate, community, guides, contact, server-status, login, profile, admin, not-found) - Add real cover images for all 4 games via Strapi CMS with getImageUrl helper - Integrate NextAuth v5 with Authentik OIDC authentication - Add new public pages: community, guides, contact, server-status - Add new protected pages: login, profile, admin dashboard - Remove legacy AFC/MercadoPago system entirely - Add Docker Compose split files for service isolation (main, auth, fusionfall, nier) - Add OpenFusion VM deployment configs (config.vm.ini, systemd service, README-VM) - Add NieR Reincarnation server guide and desktop client guide - Add architecture docs for multi-VM deployment - Add healthcheck, SSE, contact, newsletter, admin API routes - Add reusable UI components, skeleton loaders, activity feed, bookmark system - Update deployment and game server documentation --- .github/workflows/deploy.yml | 81 +++- README.md | 184 ++++--- apps/cms/config/plugins.ts | 20 + .../api/game/content-types/game/schema.json | 1 - apps/web/Dockerfile | 3 +- apps/web/next.config.ts | 47 ++ apps/web/package.json | 10 + apps/web/src/app/[locale]/about/page.tsx | 18 +- apps/web/src/app/[locale]/admin/page.tsx | 214 +++++++++ .../src/app/[locale]/afc/buy/failure/page.tsx | 33 -- apps/web/src/app/[locale]/afc/buy/page.tsx | 138 ------ .../src/app/[locale]/afc/buy/pending/page.tsx | 39 -- .../src/app/[locale]/afc/buy/success/page.tsx | 39 -- .../web/src/app/[locale]/afc/history/page.tsx | 101 ---- apps/web/src/app/[locale]/afc/page.tsx | 90 ---- apps/web/src/app/[locale]/afc/redeem/page.tsx | 184 ------- apps/web/src/app/[locale]/catalog/page.tsx | 15 +- apps/web/src/app/[locale]/community/page.tsx | 183 +++++++ apps/web/src/app/[locale]/contact/page.tsx | 116 +++++ apps/web/src/app/[locale]/donate/page.tsx | 32 +- .../games/[slug]/documentary/page.tsx | 20 + .../src/app/[locale]/games/[slug]/page.tsx | 42 ++ apps/web/src/app/[locale]/guides/page.tsx | 259 ++++++++++ apps/web/src/app/[locale]/layout.tsx | 43 +- apps/web/src/app/[locale]/login/page.tsx | 22 + apps/web/src/app/[locale]/not-found.tsx | 37 ++ apps/web/src/app/[locale]/page.tsx | 14 +- apps/web/src/app/[locale]/profile/page.tsx | 30 ++ .../src/app/[locale]/server-status/page.tsx | 79 ++++ apps/web/src/app/[locale]/template.tsx | 22 + apps/web/src/app/api/activities/route.ts | 18 + apps/web/src/app/api/admin/messages/route.ts | 41 ++ .../src/app/api/admin/subscribers/route.ts | 41 ++ apps/web/src/app/api/afc/balance/route.ts | 16 - .../app/api/afc/create-preference/route.ts | 71 --- apps/web/src/app/api/afc/lib/bridge.ts | 50 -- apps/web/src/app/api/afc/lib/mercadopago.ts | 9 - apps/web/src/app/api/afc/payments/route.ts | 16 - apps/web/src/app/api/afc/redeem/route.ts | 49 -- apps/web/src/app/api/afc/redemptions/route.ts | 16 - apps/web/src/app/api/afc/verify-disk/route.ts | 15 - apps/web/src/app/api/afc/webhook/route.ts | 113 ----- .../src/app/api/auth/[...nextauth]/route.ts | 3 + apps/web/src/app/api/contact/route.ts | 48 ++ apps/web/src/app/api/health/route.ts | 97 ++++ apps/web/src/app/api/newsletter/route.ts | 49 ++ apps/web/src/app/api/sse/route.ts | 47 ++ apps/web/src/app/globals.css | 243 +++++++++- apps/web/src/app/manifest.ts | 30 ++ apps/web/src/app/not-found.tsx | 14 +- apps/web/src/app/robots.ts | 13 + apps/web/src/app/sitemap.ts | 41 ++ .../src/components/activity/ActivityFeed.tsx | 98 ++++ .../web/src/components/admin/HealthBanner.tsx | 140 ++++++ .../src/components/admin/ServerStatusGrid.tsx | 75 +++ .../src/components/admin/SubscriberChart.tsx | 83 ++++ .../web/src/components/afc/AfcPackageCard.tsx | 49 -- .../web/src/components/afc/BalanceDisplay.tsx | 36 -- apps/web/src/components/afc/DiskIdInput.tsx | 77 --- .../components/afc/PaymentHistoryTable.tsx | 52 -- apps/web/src/components/afc/PrizeCard.tsx | 41 -- apps/web/src/components/afc/RedeemForm.tsx | 82 ---- .../components/afc/RedemptionHistoryTable.tsx | 52 -- apps/web/src/components/afc/StatusBadge.tsx | 26 - apps/web/src/components/auth/AuthProvider.tsx | 8 + apps/web/src/components/auth/LoginForm.tsx | 58 +++ apps/web/src/components/auth/ProfileCard.tsx | 105 ++++ .../components/bookmark/BookmarkButton.tsx | 126 +++++ .../src/components/catalog/CatalogFilters.tsx | 10 +- .../src/components/catalog/CatalogGrid.tsx | 40 +- .../components/catalog/CatalogSkeleton.tsx | 18 + .../components/documentary/AudioPlayer.tsx | 20 +- .../components/documentary/ChapterContent.tsx | 106 ++++- .../src/components/documentary/ChapterNav.tsx | 84 +++- .../documentary/DocumentaryLayout.tsx | 365 ++++++++++++-- .../components/documentary/GiscusComments.tsx | 33 ++ .../documentary/ReadingProgress.tsx | 36 +- apps/web/src/components/game/GameHeader.tsx | 8 +- apps/web/src/components/game/GameInfo.tsx | 24 +- .../src/components/game/ScreenshotGallery.tsx | 2 +- .../home/DocumentaryExperienceSection.tsx | 202 ++++++++ apps/web/src/components/home/DonationCTA.tsx | 33 +- .../src/components/home/DonationSection.tsx | 201 ++++++++ .../components/home/GamesShowcaseSection.tsx | 260 ++++++++++ apps/web/src/components/home/HeroSection.tsx | 212 +++++++-- apps/web/src/components/home/LatestGames.tsx | 31 +- .../src/components/home/PillarsSection.tsx | 120 +++++ .../src/components/home/TechStackSection.tsx | 140 ++++++ apps/web/src/components/layout/Footer.tsx | 15 +- .../components/layout/LanguageSwitcher.tsx | 8 +- apps/web/src/components/layout/Navbar.tsx | 194 +++++++- .../web/src/components/live/LiveIndicator.tsx | 27 ++ .../src/components/navigation/Breadcrumb.tsx | 56 +++ .../web/src/components/search/SearchInput.tsx | 74 +++ apps/web/src/components/shared/GameCard.tsx | 173 ++++++- .../components/shared/GameCardSkeleton.tsx | 12 + .../web/src/components/social/SocialShare.tsx | 119 +++++ .../src/components/theme/ThemeProvider.tsx | 21 + apps/web/src/components/theme/ThemeToggle.tsx | 13 + apps/web/src/components/ui/CookieConsent.tsx | 54 +++ apps/web/src/components/ui/NewsletterForm.tsx | 86 ++++ apps/web/src/components/ui/ScrollToTop.tsx | 40 ++ apps/web/src/components/ui/Toast.tsx | 89 ++++ apps/web/src/hooks/useDiskId.ts | 82 ---- apps/web/src/hooks/useServerStatus.ts | 55 +++ apps/web/src/hooks/useToast.tsx | 45 ++ apps/web/src/lib/activity.ts | 30 ++ apps/web/src/lib/afc.ts | 88 ---- apps/web/src/lib/api.ts | 37 +- apps/web/src/lib/auth.ts | 57 +++ apps/web/src/lib/email.ts | 70 +++ apps/web/src/lib/images.ts | 19 + apps/web/src/lib/rate-limit/simple.ts | 41 ++ apps/web/src/lib/redis.ts | 46 ++ apps/web/src/messages/en.json | 114 ++--- apps/web/src/messages/es.json | 114 ++--- apps/web/src/middleware.ts | 12 +- docker/.env.example | 143 +++++- docker/docker-compose.auth.yml | 100 ++++ docker/docker-compose.dev.yml | 170 ------- docker/docker-compose.fusionfall.yml | 36 ++ docker/docker-compose.main.yml | 264 +++++++++++ docker/docker-compose.maple2.yml | 120 ----- docker/docker-compose.nier.yml | 29 ++ docker/docker-compose.yml | 40 ++ docker/nginx/nginx.main.conf | 102 ++++ docs/architecture-vms.md | 231 +++++++++ docs/clean-install.md | 277 +++++++++++ docs/deployment.md | 2 +- docs/documentaries/dragonball-online.md | 143 ++++++ docs/documentaries/fusionfall.md | 183 +++++++ docs/documentaries/maplestory2.md | 155 ++++++ docs/documentaries/nier-reincarnation.md | 115 +++++ docs/game-servers.md | 179 ++++++- docs/nier-desktop-guide.md | 136 ++++++ docs/seeds/README.md | 98 ++++ docs/seeds/documentaries-seed.json | 224 +++++++++ package-lock.json | 447 ++++++++++++++++++ scripts/deploy-vm.sh | 105 ++++ scripts/fix-es-chapters.js | 71 +++ scripts/import-documentaries.js | 220 +++++++++ scripts/install.sh | 204 ++++++++ scripts/localize-to-es.js | 87 ++++ scripts/setup-game-vm.sh | 138 ++++++ scripts/setup-main.sh | 138 ++++++ servers/nier-reincarnation/Dockerfile | 35 ++ servers/openfusion/README-VM.md | 99 ++++ servers/openfusion/config.ini | 2 +- servers/openfusion/config.vm.ini | 34 ++ servers/openfusion/docker-entrypoint.sh | 10 +- servers/openfusion/fusionfall.service | 28 ++ 151 files changed, 10053 insertions(+), 2312 deletions(-) create mode 100644 apps/web/src/app/[locale]/admin/page.tsx delete mode 100644 apps/web/src/app/[locale]/afc/buy/failure/page.tsx delete mode 100644 apps/web/src/app/[locale]/afc/buy/page.tsx delete mode 100644 apps/web/src/app/[locale]/afc/buy/pending/page.tsx delete mode 100644 apps/web/src/app/[locale]/afc/buy/success/page.tsx delete mode 100644 apps/web/src/app/[locale]/afc/history/page.tsx delete mode 100644 apps/web/src/app/[locale]/afc/page.tsx delete mode 100644 apps/web/src/app/[locale]/afc/redeem/page.tsx create mode 100644 apps/web/src/app/[locale]/community/page.tsx create mode 100644 apps/web/src/app/[locale]/contact/page.tsx create mode 100644 apps/web/src/app/[locale]/guides/page.tsx create mode 100644 apps/web/src/app/[locale]/login/page.tsx create mode 100644 apps/web/src/app/[locale]/not-found.tsx create mode 100644 apps/web/src/app/[locale]/profile/page.tsx create mode 100644 apps/web/src/app/[locale]/server-status/page.tsx create mode 100644 apps/web/src/app/[locale]/template.tsx create mode 100644 apps/web/src/app/api/activities/route.ts create mode 100644 apps/web/src/app/api/admin/messages/route.ts create mode 100644 apps/web/src/app/api/admin/subscribers/route.ts delete mode 100644 apps/web/src/app/api/afc/balance/route.ts delete mode 100644 apps/web/src/app/api/afc/create-preference/route.ts delete mode 100644 apps/web/src/app/api/afc/lib/bridge.ts delete mode 100644 apps/web/src/app/api/afc/lib/mercadopago.ts delete mode 100644 apps/web/src/app/api/afc/payments/route.ts delete mode 100644 apps/web/src/app/api/afc/redeem/route.ts delete mode 100644 apps/web/src/app/api/afc/redemptions/route.ts delete mode 100644 apps/web/src/app/api/afc/verify-disk/route.ts delete mode 100644 apps/web/src/app/api/afc/webhook/route.ts create mode 100644 apps/web/src/app/api/auth/[...nextauth]/route.ts create mode 100644 apps/web/src/app/api/contact/route.ts create mode 100644 apps/web/src/app/api/health/route.ts create mode 100644 apps/web/src/app/api/newsletter/route.ts create mode 100644 apps/web/src/app/api/sse/route.ts create mode 100644 apps/web/src/app/manifest.ts create mode 100644 apps/web/src/app/robots.ts create mode 100644 apps/web/src/app/sitemap.ts create mode 100644 apps/web/src/components/activity/ActivityFeed.tsx create mode 100644 apps/web/src/components/admin/HealthBanner.tsx create mode 100644 apps/web/src/components/admin/ServerStatusGrid.tsx create mode 100644 apps/web/src/components/admin/SubscriberChart.tsx delete mode 100644 apps/web/src/components/afc/AfcPackageCard.tsx delete mode 100644 apps/web/src/components/afc/BalanceDisplay.tsx delete mode 100644 apps/web/src/components/afc/DiskIdInput.tsx delete mode 100644 apps/web/src/components/afc/PaymentHistoryTable.tsx delete mode 100644 apps/web/src/components/afc/PrizeCard.tsx delete mode 100644 apps/web/src/components/afc/RedeemForm.tsx delete mode 100644 apps/web/src/components/afc/RedemptionHistoryTable.tsx delete mode 100644 apps/web/src/components/afc/StatusBadge.tsx create mode 100644 apps/web/src/components/auth/AuthProvider.tsx create mode 100644 apps/web/src/components/auth/LoginForm.tsx create mode 100644 apps/web/src/components/auth/ProfileCard.tsx create mode 100644 apps/web/src/components/bookmark/BookmarkButton.tsx create mode 100644 apps/web/src/components/catalog/CatalogSkeleton.tsx create mode 100644 apps/web/src/components/documentary/GiscusComments.tsx create mode 100644 apps/web/src/components/home/DocumentaryExperienceSection.tsx create mode 100644 apps/web/src/components/home/DonationSection.tsx create mode 100644 apps/web/src/components/home/GamesShowcaseSection.tsx create mode 100644 apps/web/src/components/home/PillarsSection.tsx create mode 100644 apps/web/src/components/home/TechStackSection.tsx create mode 100644 apps/web/src/components/live/LiveIndicator.tsx create mode 100644 apps/web/src/components/navigation/Breadcrumb.tsx create mode 100644 apps/web/src/components/search/SearchInput.tsx create mode 100644 apps/web/src/components/shared/GameCardSkeleton.tsx create mode 100644 apps/web/src/components/social/SocialShare.tsx create mode 100644 apps/web/src/components/theme/ThemeProvider.tsx create mode 100644 apps/web/src/components/theme/ThemeToggle.tsx create mode 100644 apps/web/src/components/ui/CookieConsent.tsx create mode 100644 apps/web/src/components/ui/NewsletterForm.tsx create mode 100644 apps/web/src/components/ui/ScrollToTop.tsx create mode 100644 apps/web/src/components/ui/Toast.tsx delete mode 100644 apps/web/src/hooks/useDiskId.ts create mode 100644 apps/web/src/hooks/useServerStatus.ts create mode 100644 apps/web/src/hooks/useToast.tsx create mode 100644 apps/web/src/lib/activity.ts delete mode 100644 apps/web/src/lib/afc.ts create mode 100644 apps/web/src/lib/auth.ts create mode 100644 apps/web/src/lib/email.ts create mode 100644 apps/web/src/lib/images.ts create mode 100644 apps/web/src/lib/rate-limit/simple.ts create mode 100644 apps/web/src/lib/redis.ts create mode 100644 docker/docker-compose.auth.yml create mode 100644 docker/docker-compose.fusionfall.yml create mode 100644 docker/docker-compose.main.yml delete mode 100644 docker/docker-compose.maple2.yml create mode 100644 docker/docker-compose.nier.yml create mode 100644 docker/nginx/nginx.main.conf create mode 100644 docs/architecture-vms.md create mode 100644 docs/clean-install.md create mode 100644 docs/documentaries/dragonball-online.md create mode 100644 docs/documentaries/fusionfall.md create mode 100644 docs/documentaries/maplestory2.md create mode 100644 docs/documentaries/nier-reincarnation.md create mode 100644 docs/nier-desktop-guide.md create mode 100644 docs/seeds/README.md create mode 100644 docs/seeds/documentaries-seed.json create mode 100755 scripts/deploy-vm.sh create mode 100644 scripts/fix-es-chapters.js create mode 100644 scripts/import-documentaries.js create mode 100755 scripts/install.sh create mode 100644 scripts/localize-to-es.js create mode 100755 scripts/setup-game-vm.sh create mode 100755 scripts/setup-main.sh create mode 100644 servers/nier-reincarnation/Dockerfile create mode 100644 servers/openfusion/README-VM.md create mode 100644 servers/openfusion/config.vm.ini create mode 100644 servers/openfusion/fusionfall.service diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7b4a17b..dcbf786 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,26 +1,85 @@ -name: Deploy +name: Deploy Multi-VM on: push: branches: [main] jobs: - deploy: + deploy-web: + name: Deploy VM Web runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - name: Deploy to VPS + - name: Deploy to VM Web uses: appleboy/ssh-action@v1 with: - host: ${{ secrets.VPS_HOST }} - username: ${{ secrets.VPS_USER }} - key: ${{ secrets.VPS_SSH_KEY }} + host: ${{ secrets.VM_WEB_HOST }} + username: ${{ secrets.VM_WEB_USER }} + key: ${{ secrets.VM_WEB_SSH_KEY }} script: | cd /opt/project-afterlife git pull origin main cd docker - docker compose build - docker compose up -d - docker compose exec web npm run build - docker compose restart web + docker compose -f docker-compose.web.yml build + docker compose -f docker-compose.web.yml up -d + docker compose -f docker-compose.web.yml exec web npm run build + docker compose -f docker-compose.web.yml restart web + + deploy-auth: + name: Deploy VM Auth + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Deploy to VM Auth + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.VM_AUTH_HOST }} + username: ${{ secrets.VM_AUTH_USER }} + key: ${{ secrets.VM_AUTH_SSH_KEY }} + script: | + cd /opt/project-afterlife + git pull origin main + cd docker + docker compose -f docker-compose.auth.yml pull + docker compose -f docker-compose.auth.yml up -d + + deploy-games: + name: Deploy Game Servers + runs-on: ubuntu-latest + strategy: + matrix: + include: + - vm: fusionfall + host_secret: VM_FUSIONFALL_HOST + user_secret: VM_FUSIONFALL_USER + key_secret: VM_FUSIONFALL_SSH_KEY + compose: docker-compose.fusionfall.yml + - vm: maple2 + host_secret: VM_MAPLE2_HOST + user_secret: VM_MAPLE2_USER + key_secret: VM_MAPLE2_SSH_KEY + compose: docker-compose.maple2.yml + - vm: minecraft + host_secret: VM_MINECRAFT_HOST + user_secret: VM_MINECRAFT_USER + key_secret: VM_MINECRAFT_SSH_KEY + compose: docker-compose.minecraft.yml + - vm: retro + host_secret: VM_RETRO_HOST + user_secret: VM_RETRO_USER + key_secret: VM_RETRO_SSH_KEY + compose: docker-compose.retro.yml + steps: + - uses: actions/checkout@v4 + - name: Deploy to ${{ matrix.vm }} + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets[matrix.host_secret] }} + username: ${{ secrets[matrix.user_secret] }} + key: ${{ secrets[matrix.key_secret] }} + script: | + cd /opt/project-afterlife + git pull origin main + cd docker + docker compose -f ${{ matrix.compose }} build + docker compose -f ${{ matrix.compose }} up -d diff --git a/README.md b/README.md index 604bbe7..ee18cf4 100644 --- a/README.md +++ b/README.md @@ -10,58 +10,59 @@ Plataforma de preservacion de videojuegos con documentales interactivos. Servido | **Strapi 5** (CMS) | En linea | 1337 | ~179 MB | | **PostgreSQL 16** | En linea | 5432 | ~57 MB | | **MinIO** (almacenamiento) | En linea | 9000/9001 | ~144 MB | -| **OpenFusion** (FusionFall) | En linea | 23000-23001 | ~254 MB | +| **Authentik** (SSO) | En linea | 9000 | ~512 MB | +| **NieR Reincarnation** | Alpha | 80/443 | ~1 GB | +| **Dragon Ball Online** | En configuracion | 22000-22010 | ~2 GB | | **MapleStory 2 - World** | En linea | 21001 | ~126 MB | | **MapleStory 2 - Login** | En linea | 20001 | ~100 MB | -| **MapleStory 2 - Web** | En linea | 4000 | ~70 MB | | **MapleStory 2 - Game Ch0** | En linea | 20003/21003 | ~341 MB | -| **MapleStory 2 - MySQL** | En linea | 3307 | ~733 MB | -| **Minecraft FTB Evolution** | En linea | 25565 | ~3.5 GB | -| **SM64 Coop DX** | En linea | 7777/udp | ~45 MB | -| **N64 Netplay** (Mario Party) | En linea | 45000-45004 | <1 MB | -| **Dolphin Traversal** (GC/Wii) | En linea | 6262/udp, 6226/udp | <10 MB | +| **FusionFall** | En linea | 23000-23001 | ~254 MB | -**Total**: ~6 GB RAM / 40 GB disponibles | 35 GB disco / 96 GB disponibles +## Soft Launch — Juegos Disponibles -## Juegos Preservados +### NieR Reincarnation +- **Emulador**: [MariesWonderland](https://github.com/BillyCool/MariesWonderland) (C# .NET 10) +- **Conexion**: `play.consultoria-as.com:80/443` (HTTP/gRPC HTTP/2) +- **Cliente**: APK Android parcheado (via Google Colab) +- **Documental**: "El Mundo de las Voces Perdidas" (en produccion) +- **Estado**: Alpha — gameplay basico funcional -### FusionFall (Cartoon Network Universe) -- **Emulador**: [OpenFusion](https://github.com/OpenFusionProject/OpenFusion) (C++) -- **Conexion**: `play.consultoria-as.com:23000` (o `192.168.10.234:23000` en LAN) -- **Cliente**: [FusionFall Retro Client](https://github.com/OpenFusionProject) -- **Documental**: "FusionFall: El Mundo Que No Queriamos Perder" (7 capitulos) +### Dragon Ball Online +- **Emulador**: [DBO Global](https://github.com/dboglobal) (C++ / Windows) +- **Conexion**: `play.consultoria-as.com:22000` +- **Cliente**: DBO Global Client (Windows) +- **Documental**: "La Tierra Sin Goku" (en produccion) +- **Estado**: En configuracion — requiere VM Windows ### MapleStory 2 - **Emulador**: [Maple2](https://github.com/MS2Community/Maple2) (C# .NET 8) -- **Conexion**: `play.consultoria-as.com:20001` (o `192.168.10.234:20001` en LAN) +- **Conexion**: `play.consultoria-as.com:20001` - **Cliente**: MapleStory 2 Global Client + XML Patches -- **Documental**: "MapleStory 2: El Mundo Que Construimos Juntos" (7 capitulos) +- **Documental**: "El Mundo Que Construimos Juntos" (7 capitulos) +- **Estado**: Online -### Minecraft: FTB Evolution -- **Servidor**: [itzg/minecraft-server](https://github.com/itzg/docker-minecraft-server) (Java 21) -- **Conexion**: `play.consultoria-as.com:25565` (o `192.168.10.234:25565` en LAN) -- **Cliente**: FTB App o launcher compatible con FTB Evolution v1.29.1 -- **Modpack**: 200+ mods, Minecraft 1.21.1 + NeoForge 21.1.218 +### FusionFall +- **Emulador**: [OpenFusion](https://github.com/OpenFusionProject/OpenFusion) (C++) +- **Conexion**: `play.consultoria-as.com:23000` +- **Cliente**: FusionFall Retro Client +- **Documental**: "El Mundo Que No Queriamos Perder" (7 capitulos) +- **Estado**: Online -### Super Mario 64 Coop -- **Servidor**: [sm64coopdx](https://github.com/coop-deluxe/sm64coopdx) (C, headless) -- **Conexion**: `play.consultoria-as.com:7777` (o `192.168.10.234:7777` en LAN) -- **Cliente**: sm64coopdx (compilado con la misma ROM) -- **Jugadores**: Hasta 16, con mods incluidos (star-road, arena, character-select) - -### Mario Party 1-3 (N64 Netplay) -- **Servidor**: [gopher64-netplay-server](https://github.com/gopher64/gopher64-netplay-server) (Go) -- **Conexion**: `play.consultoria-as.com:45000` (o `192.168.10.234:45000` en LAN) -- **Cliente**: [gopher64](https://github.com/gopher64/gopher64) o RMG + ROM de Mario Party -- **Jugadores**: 4 por sala, 4 salas concurrentes - -### GameCube / Wii (Dolphin Netplay) -- **Servidor**: Dolphin Traversal Server (NAT hole-punching) -- **Config en Dolphin**: Traversal Server = `play.consultoria-as.com`, Port = `6262` -- **Juegos**: Mario Party 4-7, MKDD, Smash Melee, F-Zero GX, y cualquier juego de GC/Wii ## Arquitectura +### Multi-VM (Nueva Arquitectura) + +Cada componente corre en su propia VM para maximizar aislamiento y control: + +| VM | IP Privada | Puertos Públicos | Servicios | +|----|-----------|------------------|-----------| +| **vm-main** | `10.0.0.10` | `80, 443` | Web + Auth + CMS + PostgreSQL + MinIO + Nginx | +| **vm-nier** | `10.0.0.70` | `80, 443` | NieR Reincarnation (MariesWonderland) | +| **vm-dbo** | `10.0.0.80` | `22000-22010` | Dragon Ball Online (DBO Global) | +| **vm-maple2** | `10.0.0.40` | `20001, 21001, 20003, 21003, 4000` | MapleStory 2 | +| **vm-fusionfall** | `10.0.0.30` | `23000, 23001` | OpenFusion Server | + ``` project-afterlife/ ├── apps/ @@ -72,15 +73,35 @@ project-afterlife/ ├── servers/ │ ├── openfusion/ # Servidor FusionFall (C++) │ ├── maple2/ # Servidor MapleStory 2 (C# .NET 8) -│ ├── sm64coopdx/ # Super Mario 64 Coop (C, headless) -│ └── dolphin-traversal/ # Dolphin Traversal Server (C++) +│ ├── nier-reincarnation/ # NieR Reincarnation (MariesWonderland .NET 10) +│ └── dragonball-online/ # Dragon Ball Online (DBO Global C++) +├── services/ +│ └── afc-bridge/ # Bridge API blockchain (Node.js) — legacy +├── blockchain/ +│ ├── contracts/AfterCoin.sol # Contrato inteligente ERC-20 — legacy +│ ├── genesis.json # Config genesis Geth — legacy +│ └── Dockerfile # Nodo Geth — legacy ├── docker/ -│ ├── docker-compose.dev.yml # Stack local (web + CMS + juegos) -│ ├── docker-compose.maple2.yml # MapleStory 2 (separado) -│ ├── docker-compose.yml # Produccion (con Nginx + SSL) -│ └── nginx/ # Configuracion Nginx -├── docs/ # Documentacion del proyecto -└── .github/workflows/ # CI/CD deployment +│ ├── docker-compose.main.yml # VM Principal (Web + Auth + CMS) +│ ├── docker-compose.nier.yml # VM NieR Reincarnation +│ ├── docker-compose.dbo.yml # VM Dragon Ball Online +│ ├── docker-compose.fusionfall.yml # VM OpenFusion +│ ├── docker-compose.maple2.yml # VM MapleStory 2 +│ ├── docker-compose.dev.yml # Legacy: stack local completo +│ ├── docker-compose.yml # Legacy: produccion monolitica +│ ├── docker-compose.web.yml # Legacy: web separado +│ ├── docker-compose.auth.yml # Legacy: auth separado +│ └── nginx/ # Configuracion Nginx +├── scripts/ +│ └── deploy-vm.sh # Script helper para deploy por VM +├── docs/ +│ ├── architecture.md # Arquitectura tecnica detallada +│ ├── architecture-vms.md # Documentacion multi-VM +│ ├── game-servers.md # Setup de servidores de juegos +│ ├── cms-content.md # Modelo de contenido CMS +│ └── deployment.md # Guia de despliegue +└── .github/workflows/ + └── deploy.yml # CI/CD multi-VM ``` ### Stack Tecnologico @@ -102,19 +123,55 @@ project-afterlife/ ## Inicio Rapido -### Requisitos +### Instalacion Limpia (Recomendado para Produccion) + +Ver `docs/clean-install.md` para la guia completa paso a paso. + +#### Resumen rapido + +**VM Principal** (Web + Auth + CMS): +```bash +# 1. Preparar la VM (instala Docker, firewall, genera secrets) +./scripts/setup-main.sh + +# 2. Clonar y configurar +git clone https://git.consultoria-as.com/consultoria-as/project-afterlife.git /opt/project-afterlife +cd /opt/project-afterlife +cp docker/.env.example docker/.env +# Edita docker/.env con los secrets generados + +# 3. Instalar +./scripts/install.sh main +``` + +**VMs de Juegos** (una por juego): +```bash +# Preparar VM de juego (ejemplo: NieR) +./scripts/setup-game-vm.sh nier + +# Clonar e instalar +git clone ... /opt/project-afterlife +cd /opt/project-afterlife +./scripts/install.sh nier +``` + +### Stack Local Completo (Desarrollo / Monolito Legacy) + +Para desarrollo local donde todo corre en una sola maquina: + +#### Requisitos - Docker y Docker Compose v2+ - 8 GB RAM minimo (16 GB recomendado con todos los servidores) - 50 GB disco libre -### 1. Clonar y configurar +#### 1. Clonar y configurar ```bash git clone https://git.consultoria-as.com/consultoria-as/project-afterlife.git cd project-afterlife ``` -### 2. Crear archivo de entorno +#### 2. Crear archivo de entorno ```bash cp docker/.env.example docker/.env @@ -144,24 +201,27 @@ STRAPI_API_TOKEN= # Strapi URL publica PUBLIC_STRAPI_URL=http://localhost:1337 - -# OpenFusion -OPENFUSION_SHARD_IP=192.168.10.234 ``` -### 3. Levantar servicios base +#### 3. Levantar servicios base ```bash cd docker -# Stack principal (CMS + Web + OpenFusion + Minecraft FTB) +# Stack principal (CMS + Web) docker compose -f docker-compose.dev.yml up -d +# NieR Reincarnation (requiere setup previo, ver docs/game-servers.md) +docker compose -f docker-compose.nier.yml up -d + # MapleStory 2 (requiere setup previo, ver docs/game-servers.md) docker compose -f docker-compose.maple2.yml up -d + +# Dragon Ball Online (requiere setup previo, ver docs/game-servers.md) +docker compose -f docker-compose.dbo.yml up -d ``` -### 4. Setup inicial de Strapi +#### 4. Setup inicial de Strapi 1. Abrir http://localhost:1337/admin 2. Crear usuario administrador @@ -169,7 +229,7 @@ docker compose -f docker-compose.maple2.yml up -d 4. Tipo: Full access, copiar el token a `STRAPI_API_TOKEN` en `.env` 5. Reiniciar el servicio web: `docker compose -f docker-compose.dev.yml restart web` -### 5. Verificar +#### 5. Verificar - **Frontend**: http://localhost:3000 - **CMS Admin**: http://localhost:1337/admin @@ -196,23 +256,29 @@ docker compose -f docker-compose.maple2.yml up -d | `/es/donate` | Pagina de donaciones | | `/es/games/[slug]` | Pagina individual de juego | | `/es/games/[slug]/documentary` | Documental interactivo | +| `/es/login` | Iniciar sesion con Authentik | +| `/es/profile` | Perfil de usuario | +| `/es/server-status` | Estado de todos los servidores | ## Contenido en Base de Datos -### Juegos +### Juegos (Soft Launch) | Slug | Titulo | Estado | Documental | |------|--------|--------|------------| -| `fusionfall` | FusionFall | Online | 7 capitulos | +| `nier-reincarnation` | NieR Reincarnation | Alpha | En produccion | +| `dragonball-online` | Dragon Ball Online | En configuracion | En produccion | | `maplestory2` | MapleStory 2 | Online | 7 capitulos | -| `minecraft-ftb-evolution` | Minecraft: FTB Evolution | Online | Pendiente | +| `fusionfall` | FusionFall | Online | 7 capitulos | ### Documentales | Juego | Titulo | Capitulos | |-------|--------|-----------| +| NieR Reincarnation | "El Mundo de las Voces Perdidas" | En produccion | +| Dragon Ball Online | "La Tierra Sin Goku" | En produccion | | FusionFall | "El Mundo Que No Queriamos Perder" | 7 | | MapleStory 2 | "El Mundo Que Construimos Juntos" | 7 | -Cada documental tiene sus 7 capitulos publicados en ambos idiomas (ES/EN). +Cada documental publicado tiene sus capitulos disponibles en ambos idiomas (ES/EN). ## Licencia diff --git a/apps/cms/config/plugins.ts b/apps/cms/config/plugins.ts index 5555bf5..305568b 100644 --- a/apps/cms/config/plugins.ts +++ b/apps/cms/config/plugins.ts @@ -6,4 +6,24 @@ export default () => ({ locales: ["es", "en"], }, }, + "users-permissions": { + config: { + providers: { + // Authentik OIDC provider for CMS admin SSO + authentik: { + enabled: true, + icon: "authentik", + key: "", + secret: "", + callback: `${process.env.PUBLIC_STRAPI_URL || "http://localhost:1337"}/api/auth/authentik/callback`, + scope: ["openid", "email", "profile"], + // Authentik endpoints + authorization_endpoint: `${process.env.AUTHENTIK_URL || "http://10.0.0.20:9000"}/application/o/authorize/`, + access_token_endpoint: `${process.env.AUTHENTIK_URL || "http://10.0.0.20:9000"}/application/o/token/`, + access_token_params: {}, + grant_type: "authorization_code", + }, + }, + }, + }, }); diff --git a/apps/cms/src/api/game/content-types/game/schema.json b/apps/cms/src/api/game/content-types/game/schema.json index 78f9a36..b647c04 100644 --- a/apps/cms/src/api/game/content-types/game/schema.json +++ b/apps/cms/src/api/game/content-types/game/schema.json @@ -62,7 +62,6 @@ "coverImage": { "type": "media", "multiple": false, - "required": true, "allowedTypes": ["images"] }, "serverStatus": { diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index fb8cea7..9c7e6db 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -5,7 +5,8 @@ COPY package.json package-lock.json* turbo.json ./ COPY apps/web/package.json ./apps/web/ COPY packages/shared/package.json ./packages/shared/ -RUN npm ci +# Remove stale lockfile to force fresh install +RUN rm -f package-lock.json && npm install COPY packages/shared/ ./packages/shared/ COPY apps/web/ ./apps/web/ diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 67d4bf9..e8ddc91 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -2,10 +2,57 @@ import createNextIntlPlugin from "next-intl/plugin"; const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); +const cmsUrl = process.env.NEXT_PUBLIC_STRAPI_URL || process.env.STRAPI_URL || "http://localhost:1337"; +const cmsHostname = new URL(cmsUrl).hostname; +const cmsPort = new URL(cmsUrl).port || undefined; + const nextConfig = { eslint: { ignoreDuringBuilds: true, }, + images: { + remotePatterns: [ + { + protocol: "http" as const, + hostname: cmsHostname, + port: cmsPort, + pathname: "/uploads/**", + }, + ], + }, + async headers() { + return [ + { + source: "/(.*)", + headers: [ + { + key: "X-Frame-Options", + value: "DENY", + }, + { + key: "X-Content-Type-Options", + value: "nosniff", + }, + { + key: "Referrer-Policy", + value: "strict-origin-when-cross-origin", + }, + { + key: "X-DNS-Prefetch-Control", + value: "on", + }, + { + key: "Strict-Transport-Security", + value: "max-age=63072000; includeSubDomains; preload", + }, + { + key: "Permissions-Policy", + value: "camera=(), microphone=(), geolocation=()", + }, + ], + }, + ]; + }, }; export default withNextIntl(nextConfig); diff --git a/apps/web/package.json b/apps/web/package.json index 935408a..e2f1abd 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -10,12 +10,20 @@ }, "dependencies": { "@afterlife/shared": "*", + "@giscus/react": "^3.1.0", + "@vercel/analytics": "^2.0.1", + "chart.js": "^4.5.1", "framer-motion": "^12.34.3", "howler": "^2.2.4", + "ioredis": "^5.10.1", "mercadopago": "^2.12.0", "next": "^15", + "next-auth": "^5.0.0-beta.25", "next-intl": "^4.8.3", + "nodemailer": "^6.10.1", + "pg": "^8.20.0", "react": "^19", + "react-chartjs-2": "^5.3.1", "react-dom": "^19", "uuid": "^13.0.0" }, @@ -23,6 +31,8 @@ "@tailwindcss/postcss": "^4", "@types/howler": "^2.2.12", "@types/node": "^20", + "@types/nodemailer": "^8.0.0", + "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", diff --git a/apps/web/src/app/[locale]/about/page.tsx b/apps/web/src/app/[locale]/about/page.tsx index 53be660..0ec25ec 100644 --- a/apps/web/src/app/[locale]/about/page.tsx +++ b/apps/web/src/app/[locale]/about/page.tsx @@ -1,11 +1,23 @@ +import type { Metadata } from "next"; import { useTranslations } from "next-intl"; +export function generateMetadata(): Metadata { + return { + title: "About Us", + }; +} + export default function AboutPage() { const t = useTranslations("about"); return (
-

{t("title")}

+

+ {t("title")} +

{t("mission")}

@@ -22,8 +34,8 @@ export default function AboutPage() {

{t("team")}

-
-

Team members coming soon.

+
+

Team members coming soon.

diff --git a/apps/web/src/app/[locale]/admin/page.tsx b/apps/web/src/app/[locale]/admin/page.tsx new file mode 100644 index 0000000..9383b56 --- /dev/null +++ b/apps/web/src/app/[locale]/admin/page.tsx @@ -0,0 +1,214 @@ +"use client"; + +import { useEffect, useState, useMemo } from "react"; +import { SubscriberChart } from "@/components/admin/SubscriberChart"; + +interface Subscriber { + id: number; + email: string; + locale: string; + created_at: string; +} + +interface ContactMessage { + id: number; + name: string; + email: string; + subject: string | null; + message: string; + created_at: string; +} + +function exportCSV(data: Subscriber[]) { + const headers = ["ID", "Email", "Locale", "Created At"]; + const rows = data.map((s) => [s.id, s.email, s.locale, s.created_at]); + const csv = [headers, ...rows].map((r) => r.join(",")).join("\n"); + const blob = new Blob([csv], { type: "text/csv" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `subscribers-${new Date().toISOString().split("T")[0]}.csv`; + a.click(); + URL.revokeObjectURL(url); +} + +function formatDate(iso: string) { + return new Date(iso).toLocaleDateString("es-ES", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +export default function AdminPage() { + const [apiKey, setApiKey] = useState(""); + const [subscribers, setSubscribers] = useState([]); + const [messages, setMessages] = useState([]); + const [subTotal, setSubTotal] = useState(0); + const [msgTotal, setMsgTotal] = useState(0); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + async function fetchData() { + if (!apiKey.trim()) return; + setLoading(true); + setError(""); + + try { + const [subRes, msgRes] = await Promise.all([ + fetch("/api/admin/subscribers?limit=50", { + headers: { "x-admin-key": apiKey }, + }), + fetch("/api/admin/messages?limit=50", { + headers: { "x-admin-key": apiKey }, + }), + ]); + + if (!subRes.ok || !msgRes.ok) { + setError("Invalid API key or unauthorized"); + setLoading(false); + return; + } + + const subData = await subRes.json(); + const msgData = await msgRes.json(); + + setSubscribers(subData.subscribers || []); + setSubTotal(subData.total || 0); + setMessages(msgData.messages || []); + setMsgTotal(msgData.total || 0); + } catch { + setError("Failed to fetch data"); + } finally { + setLoading(false); + } + } + + return ( +
+

+ Admin Dashboard +

+ +
+ setApiKey(e.target.value)} + placeholder="Enter admin API key" + className="flex-1 bg-[#12121a] border border-[rgba(255,255,255,0.08)] rounded-lg px-4 py-2.5 text-sm text-[#f5f5f7] focus:outline-none focus:border-[rgba(212,165,116,0.4)]" + /> + + {subscribers.length > 0 && ( + + )} +
+ + {error && ( +
+ {error} +
+ )} + + {/* Stats */} +
+
+
{subTotal}
+
Newsletter Subscribers
+
+
+
{msgTotal}
+
Contact Messages
+
+
+ + {/* Subscribers Chart */} + {subscribers.length > 0 && ( +
+

Subscriber Growth

+ { + const map = new Map(); + subscribers.forEach((s) => { + const date = new Date(s.created_at).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + map.set(date, (map.get(date) || 0) + 1); + }); + return Array.from(map.entries()).map(([date, count]) => ({ date, count })); + }, [subscribers])} + /> +
+ )} + + {/* Subscribers Table */} + {subscribers.length > 0 && ( +
+

Recent Subscribers

+
+ + + + + + + + + + {subscribers.map((sub) => ( + + + + + + ))} + +
EmailLocaleDate
{sub.email}{sub.locale}{formatDate(sub.created_at)}
+
+
+ )} + + {/* Messages Table */} + {messages.length > 0 && ( +
+

Recent Contact Messages

+
+ {messages.map((msg) => ( +
+
+
+ {msg.name} + <{msg.email}> +
+ {formatDate(msg.created_at)} +
+ {msg.subject &&
{msg.subject}
} +
{msg.message}
+
+ ))} +
+
+ )} +
+ ); +} diff --git a/apps/web/src/app/[locale]/afc/buy/failure/page.tsx b/apps/web/src/app/[locale]/afc/buy/failure/page.tsx deleted file mode 100644 index b18eb43..0000000 --- a/apps/web/src/app/[locale]/afc/buy/failure/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { useLocale, useTranslations } from "next-intl"; - -export default function BuyFailurePage() { - const t = useTranslations("afc"); - const locale = useLocale(); - - return ( -
-
- -
-

{t("payment_failure_title")}

-

{t("payment_failure_description")}

-
- - {t("try_again")} - - - {t("back_to_store")} - -
-
- ); -} diff --git a/apps/web/src/app/[locale]/afc/buy/page.tsx b/apps/web/src/app/[locale]/afc/buy/page.tsx deleted file mode 100644 index ee7b72a..0000000 --- a/apps/web/src/app/[locale]/afc/buy/page.tsx +++ /dev/null @@ -1,138 +0,0 @@ -"use client"; - -import { useState } from "react"; -import Link from "next/link"; -import { useLocale, useTranslations } from "next-intl"; -import { useDiskId } from "@/hooks/useDiskId"; -import { createPreference } from "@/lib/afc"; -import { DiskIdInput } from "@/components/afc/DiskIdInput"; -import { BalanceDisplay } from "@/components/afc/BalanceDisplay"; -import { AfcPackageCard } from "@/components/afc/AfcPackageCard"; - -const PRICE_PER_AFC = 15; - -const PACKAGES = [ - { amount: 10, popular: false }, - { amount: 25, popular: true }, - { amount: 50, popular: false }, - { amount: 100, popular: false }, -]; - -export default function BuyAfcPage() { - const t = useTranslations("afc"); - const locale = useLocale(); - const disk = useDiskId(); - const [customAmount, setCustomAmount] = useState(""); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - async function handleBuy(amount: number) { - if (!disk.verified || !disk.diskId) return; - setLoading(true); - setError(null); - try { - const data = await createPreference(disk.diskId, amount); - // Redirect to MercadoPago checkout - window.location.href = data.initPoint; - } catch (e: any) { - setError(e.message); - setLoading(false); - } - } - - return ( -
- {/* Back */} - - ← {t("back_to_store")} - - -

{t("buy_title")}

-

{t("buy_subtitle")}

- - {/* Disk ID */} -
- disk.verify(disk.diskId)} - loading={disk.loading} - verified={disk.verified} - playerName={disk.playerName} - error={disk.error} - onClear={disk.clear} - /> -
- - {disk.verified && ( - <> - {/* Balance */} -
- -
- - {/* Packages */} -
-

{t("select_package")}

- {PACKAGES.map((pkg) => ( - handleBuy(pkg.amount)} - /> - ))} -
- - {/* Custom amount */} -
-

{t("custom_amount")}

-
-
- setCustomAmount(e.target.value)} - placeholder="AFC" - className="w-full bg-gray-800 border border-white/10 rounded-xl px-4 py-3 text-white placeholder:text-gray-600 focus:outline-none focus:border-amber-500/50 transition-all" - /> - {customAmount && Number(customAmount) > 0 && ( - - = ${Number(customAmount) * PRICE_PER_AFC} MXN - - )} -
- -
-
- - {error && ( -
- {error} -
- )} - - {/* Payment info */} -

- {t("payment_info")} -

- - )} -
- ); -} diff --git a/apps/web/src/app/[locale]/afc/buy/pending/page.tsx b/apps/web/src/app/[locale]/afc/buy/pending/page.tsx deleted file mode 100644 index 762de04..0000000 --- a/apps/web/src/app/[locale]/afc/buy/pending/page.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { useLocale, useTranslations } from "next-intl"; -import { useSearchParams } from "next/navigation"; - -export default function BuyPendingPage() { - const t = useTranslations("afc"); - const locale = useLocale(); - const searchParams = useSearchParams(); - const paymentId = searchParams.get("payment_id"); - - return ( -
-
- -
-

{t("payment_pending_title")}

-

{t("payment_pending_description")}

- {paymentId && ( -

ID: {paymentId}

- )} -
- - {t("view_history")} - - - {t("back_to_store")} - -
-
- ); -} diff --git a/apps/web/src/app/[locale]/afc/buy/success/page.tsx b/apps/web/src/app/[locale]/afc/buy/success/page.tsx deleted file mode 100644 index c83cf89..0000000 --- a/apps/web/src/app/[locale]/afc/buy/success/page.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { useLocale, useTranslations } from "next-intl"; -import { useSearchParams } from "next/navigation"; - -export default function BuySuccessPage() { - const t = useTranslations("afc"); - const locale = useLocale(); - const searchParams = useSearchParams(); - const paymentId = searchParams.get("payment_id"); - - return ( -
-
- -
-

{t("payment_success_title")}

-

{t("payment_success_description")}

- {paymentId && ( -

ID: {paymentId}

- )} -
- - {t("back_to_store")} - - - {t("view_history")} - -
-
- ); -} diff --git a/apps/web/src/app/[locale]/afc/history/page.tsx b/apps/web/src/app/[locale]/afc/history/page.tsx deleted file mode 100644 index 87522fe..0000000 --- a/apps/web/src/app/[locale]/afc/history/page.tsx +++ /dev/null @@ -1,101 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import Link from "next/link"; -import { useLocale, useTranslations } from "next-intl"; -import { useDiskId } from "@/hooks/useDiskId"; -import { getPaymentHistory, getRedemptionHistory } from "@/lib/afc"; -import type { Payment, Redemption } from "@/lib/afc"; -import { DiskIdInput } from "@/components/afc/DiskIdInput"; -import { BalanceDisplay } from "@/components/afc/BalanceDisplay"; -import { PaymentHistoryTable } from "@/components/afc/PaymentHistoryTable"; -import { RedemptionHistoryTable } from "@/components/afc/RedemptionHistoryTable"; - -type Tab = "payments" | "redemptions"; - -export default function HistoryPage() { - const t = useTranslations("afc"); - const locale = useLocale(); - const disk = useDiskId(); - const [tab, setTab] = useState("payments"); - const [payments, setPayments] = useState([]); - const [redemptions, setRedemptions] = useState([]); - const [loadingData, setLoadingData] = useState(false); - - useEffect(() => { - if (!disk.verified || !disk.diskId) return; - setLoadingData(true); - Promise.all([ - getPaymentHistory(disk.diskId).then((d) => setPayments(d.payments || [])).catch(() => {}), - getRedemptionHistory(disk.diskId).then((d) => setRedemptions(d.redemptions || [])).catch(() => {}), - ]).finally(() => setLoadingData(false)); - }, [disk.verified, disk.diskId]); - - return ( -
- - ← {t("back_to_store")} - - -

{t("history_title")}

-

{t("history_subtitle")}

- - {/* Disk ID */} -
- disk.verify(disk.diskId)} - loading={disk.loading} - verified={disk.verified} - playerName={disk.playerName} - error={disk.error} - onClear={disk.clear} - /> -
- - {disk.verified && ( - <> -
- -
- - {/* Tabs */} -
- - -
- - {loadingData ? ( -
{t("loading")}
- ) : tab === "payments" ? ( - - ) : ( - - )} - - )} -
- ); -} diff --git a/apps/web/src/app/[locale]/afc/page.tsx b/apps/web/src/app/[locale]/afc/page.tsx deleted file mode 100644 index 1cd4aee..0000000 --- a/apps/web/src/app/[locale]/afc/page.tsx +++ /dev/null @@ -1,90 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { useLocale, useTranslations } from "next-intl"; -import { useDiskId } from "@/hooks/useDiskId"; -import { DiskIdInput } from "@/components/afc/DiskIdInput"; -import { BalanceDisplay } from "@/components/afc/BalanceDisplay"; - -export default function AfcHubPage() { - const t = useTranslations("afc"); - const locale = useLocale(); - const disk = useDiskId(); - - return ( -
- {/* Header */} -
-
- A -
-

{t("store_title")}

-

- {t("store_subtitle")} -

-
- - {/* Disk ID */} -
- disk.verify(disk.diskId)} - loading={disk.loading} - verified={disk.verified} - playerName={disk.playerName} - error={disk.error} - onClear={disk.clear} - /> -
- - {/* Balance */} - {disk.verified && ( -
- -
- )} - - {/* Action cards */} -
- -
- + -
-

{t("buy_title")}

-

{t("buy_description")}

- - - -
- -
-

{t("redeem_title")}

-

{t("redeem_description")}

- - - -
- -
-

{t("history_title")}

-

{t("history_description")}

- -
- - {/* Info */} -
-

{t("store_info")}

-
-
- ); -} diff --git a/apps/web/src/app/[locale]/afc/redeem/page.tsx b/apps/web/src/app/[locale]/afc/redeem/page.tsx deleted file mode 100644 index ca1d8e8..0000000 --- a/apps/web/src/app/[locale]/afc/redeem/page.tsx +++ /dev/null @@ -1,184 +0,0 @@ -"use client"; - -import { useState } from "react"; -import Link from "next/link"; -import { useLocale, useTranslations } from "next-intl"; -import { useDiskId } from "@/hooks/useDiskId"; -import { redeemAfc } from "@/lib/afc"; -import { DiskIdInput } from "@/components/afc/DiskIdInput"; -import { BalanceDisplay } from "@/components/afc/BalanceDisplay"; -import { PrizeCard } from "@/components/afc/PrizeCard"; -import { RedeemForm } from "@/components/afc/RedeemForm"; - -interface Prize { - icon: string; - brand: string; - label: string; - costAfc: number; - valueMxn: number; - prizeType: string; - prizeDetail: string; -} - -const GIFT_CARDS: Prize[] = [ - { icon: "🎮", brand: "Steam", label: "$200 MXN", costAfc: 14, valueMxn: 200, prizeType: "gift_card", prizeDetail: "Steam $200 MXN" }, - { icon: "🎮", brand: "Steam", label: "$500 MXN", costAfc: 34, valueMxn: 500, prizeType: "gift_card", prizeDetail: "Steam $500 MXN" }, - { icon: "🎮", brand: "Steam", label: "$1,000 MXN", costAfc: 67, valueMxn: 1000, prizeType: "gift_card", prizeDetail: "Steam $1,000 MXN" }, - { icon: "🟢", brand: "Xbox", label: "$200 MXN", costAfc: 14, valueMxn: 200, prizeType: "gift_card", prizeDetail: "Xbox $200 MXN" }, - { icon: "🟢", brand: "Xbox", label: "$500 MXN", costAfc: 34, valueMxn: 500, prizeType: "gift_card", prizeDetail: "Xbox $500 MXN" }, - { icon: "🔵", brand: "PlayStation", label: "$200 MXN", costAfc: 14, valueMxn: 200, prizeType: "gift_card", prizeDetail: "PlayStation $200 MXN" }, - { icon: "🔵", brand: "PlayStation", label: "$500 MXN", costAfc: 14, valueMxn: 500, prizeType: "gift_card", prizeDetail: "PlayStation $500 MXN" }, - { icon: "📦", brand: "Amazon", label: "$200 MXN", costAfc: 14, valueMxn: 200, prizeType: "gift_card", prizeDetail: "Amazon $200 MXN" }, - { icon: "📦", brand: "Amazon", label: "$500 MXN", costAfc: 34, valueMxn: 500, prizeType: "gift_card", prizeDetail: "Amazon $500 MXN" }, -]; - -const CASH_OUT: Prize[] = [ - { icon: "🏦", brand: "Banco (CLABE)", label: "$750+ MXN", costAfc: 50, valueMxn: 750, prizeType: "bank_transfer", prizeDetail: "Transferencia bancaria $750 MXN" }, - { icon: "💳", brand: "MercadoPago", label: "$750+ MXN", costAfc: 50, valueMxn: 750, prizeType: "mercadopago", prizeDetail: "Retiro MercadoPago $750 MXN" }, -]; - -export default function RedeemPage() { - const t = useTranslations("afc"); - const locale = useLocale(); - const disk = useDiskId(); - const [selected, setSelected] = useState(null); - const [loading, setLoading] = useState(false); - const [success, setSuccess] = useState(false); - const [error, setError] = useState(null); - - async function handleRedeem(deliveryInfo: string) { - if (!selected || !disk.diskId) return; - setLoading(true); - setError(null); - try { - await redeemAfc({ - diskId: disk.diskId, - amountAfc: selected.costAfc, - prizeType: selected.prizeType, - prizeDetail: selected.prizeDetail, - deliveryInfo, - }); - setSuccess(true); - disk.refreshBalance(); - } catch (e: any) { - setError(e.message); - } finally { - setLoading(false); - } - } - - if (success) { - return ( -
-
- -
-

{t("redeem_success_title")}

-

{t("redeem_success_description")}

-
- - {t("view_history")} - - - {t("back_to_store")} - -
-
- ); - } - - return ( -
- - ← {t("back_to_store")} - - -

{t("redeem_title")}

-

{t("redeem_subtitle")}

- - {/* Disk ID */} -
- disk.verify(disk.diskId)} - loading={disk.loading} - verified={disk.verified} - playerName={disk.playerName} - error={disk.error} - onClear={disk.clear} - /> -
- - {disk.verified && ( - <> -
- -
- - {selected ? ( - setSelected(null)} - loading={loading} - /> - ) : ( - <> - {/* Gift Cards */} -

{t("gift_cards")}

-
- {GIFT_CARDS.map((prize, i) => ( - setSelected(prize)} - /> - ))} -
- - {/* Cash Out */} -

{t("cash_out")}

-
- {CASH_OUT.map((prize, i) => ( - setSelected(prize)} - /> - ))} -
- - )} - - {error && ( -
- {error} -
- )} - - )} -
- ); -} diff --git a/apps/web/src/app/[locale]/catalog/page.tsx b/apps/web/src/app/[locale]/catalog/page.tsx index 3bb467a..4317b5e 100644 --- a/apps/web/src/app/[locale]/catalog/page.tsx +++ b/apps/web/src/app/[locale]/catalog/page.tsx @@ -1,8 +1,13 @@ +import type { Metadata } from "next"; import { Suspense } from "react"; import { getGames } from "@/lib/api"; import { CatalogFilters } from "@/components/catalog/CatalogFilters"; import { CatalogGrid } from "@/components/catalog/CatalogGrid"; +export const metadata: Metadata = { + title: "Game Catalog | Project Afterlife", +}; + export default async function CatalogPage({ params, }: { @@ -20,8 +25,14 @@ export default async function CatalogPage({ return (
-

- {locale === "es" ? "Catálogo de Juegos" : "Game Catalog"} +

+ {locale === "es" ? "Catálogo de " : "Game "} + + {locale === "es" ? "Juegos" : "Catalog"} +

diff --git a/apps/web/src/app/[locale]/community/page.tsx b/apps/web/src/app/[locale]/community/page.tsx new file mode 100644 index 0000000..5a8120a --- /dev/null +++ b/apps/web/src/app/[locale]/community/page.tsx @@ -0,0 +1,183 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { motion } from "framer-motion"; +import { useLocale } from "next-intl"; +import Link from "next/link"; + +function DiscordIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +function GitHubIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +function HeartIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +function MessageIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +const cardVariants = { + hidden: { opacity: 0, y: 20 }, + visible: (i: number) => ({ + opacity: 1, + y: 0, + transition: { delay: i * 0.1, duration: 0.5, ease: "easeOut" as const }, + }), +}; + +const channels = [ + { + name: "Discord", + description: "Join our community Discord to chat with other players, get support, and stay updated on new releases.", + href: "https://discord.gg/projectafterlife", + icon: DiscordIcon, + color: "text-[#d4a574]", + bg: "bg-[rgba(212,165,116,0.08)] border-[rgba(212,165,116,0.15)] hover:border-[rgba(212,165,116,0.35)]", + btn: "bg-[#d4a574] hover:bg-[#e8c4a0] text-[#0a0a0f]", + btnText: "text-[#0a0a0f]", + label: "Join Discord", + }, + { + name: "GitHub", + description: "Our code is open source. Contribute to the project, report issues, or explore our repositories.", + href: "https://github.com/projectafterlife", + icon: GitHubIcon, + color: "text-[#a0a0a8]", + bg: "bg-[rgba(255,255,255,0.03)] border-[rgba(255,255,255,0.08)] hover:border-[rgba(255,255,255,0.15)]", + btn: "bg-[#1a1a24] hover:bg-[#2a2a34] text-[#f5f5f7]", + btnText: "text-[#f5f5f7]", + label: "View GitHub", + }, + { + name: "Forums", + description: "Long-form discussions, guides, bug reports, and feature requests. The heart of our community.", + href: "#", + icon: MessageIcon, + color: "text-[#e8c4a0]", + bg: "bg-[rgba(232,196,160,0.08)] border-[rgba(232,196,160,0.15)] hover:border-[rgba(232,196,160,0.35)]", + btn: "bg-[#e8c4a0] hover:bg-[#d4a574] text-[#0a0a0f]", + btnText: "text-[#0a0a0f]", + label: "Coming Soon", + }, + { + name: "Contribute", + description: "Help us preserve gaming history. We need developers, writers, translators, and testers.", + href: "/donate", + icon: HeartIcon, + color: "text-rose-400", + bg: "bg-rose-500/8 border-rose-500/15 hover:border-rose-500/35", + btn: "bg-rose-600 hover:bg-rose-500 text-white", + btnText: "text-white", + label: "How to Help", + }, +]; + +export default function CommunityPage() { + const locale = useLocale(); + const isEs = locale === "es"; + + return ( +
+ +

+ {isEs ? "Comunidad" : "Community"} +

+

+ {isEs + ? "Project Afterlife es impulsado por su comunidad. Únete a nosotros para preservar la historia de los juegos juntos." + : "Project Afterlife is driven by its community. Join us in preserving gaming history together."} +

+
+ +
+ {channels.map((channel, i) => { + const Icon = channel.icon; + const isExternal = channel.href.startsWith("http"); + const Wrapper = isExternal ? "a" : Link; + const wrapperProps = isExternal + ? { href: channel.href, target: "_blank", rel: "noopener noreferrer" } + : { href: channel.href }; + + return ( + + +
+
+ +
+
+

{channel.name}

+

+ {channel.description} +

+ + {channel.label} + +
+
+
+
+ ); + })} +
+ + +

+ {isEs ? "Código de Conducta" : "Code of Conduct"} +

+

+ {isEs + ? "Todos los miembros de nuestra comunidad deben tratar a los demás con respeto. No se tolera el acoso, la discriminación ni el comportamiento tóxico. Queremos que este sea un espacio seguro y acogedor para todos los amantes de los juegos." + : "All members of our community must treat others with respect. Harassment, discrimination, and toxic behavior are not tolerated. We want this to be a safe and welcoming space for all game lovers."} +

+
+
+ ); +} diff --git a/apps/web/src/app/[locale]/contact/page.tsx b/apps/web/src/app/[locale]/contact/page.tsx new file mode 100644 index 0000000..d887507 --- /dev/null +++ b/apps/web/src/app/[locale]/contact/page.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { useState } from "react"; +import { useLocale } from "next-intl"; +import { motion } from "framer-motion"; +import { useToast } from "@/hooks/useToast"; + +export default function ContactPage() { + const locale = useLocale(); + const isEs = locale === "es"; + const toast = useToast(); + const [form, setForm] = useState({ name: "", email: "", subject: "", message: "" }); + const [loading, setLoading] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!form.name || !form.email || !form.message) return; + + setLoading(true); + try { + const res = await fetch("/api/contact", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(form), + }); + + const data = await res.json(); + if (res.ok) { + toast.success(isEs ? "¡Mensaje enviado!" : "Message sent!"); + setForm({ name: "", email: "", subject: "", message: "" }); + } else { + toast.error(data.error || (isEs ? "Error al enviar" : "Failed to send")); + } + } catch { + toast.error(isEs ? "Error de conexión" : "Connection error"); + } finally { + setLoading(false); + } + } + + return ( +
+ +

+ {isEs ? "Contacto" : "Contact Us"} +

+

+ {isEs + ? "¿Tienes preguntas, sugerencias o quieres contribuir? Escríbenos." + : "Have questions, suggestions, or want to contribute? Reach out to us."} +

+
+ +
+
+
+ + setForm({ ...form, name: e.target.value })} + required + className="w-full bg-[#12121a] border border-[rgba(255,255,255,0.08)] rounded-lg px-4 py-2.5 text-sm text-[#f5f5f7] focus:outline-none focus:border-[rgba(212,165,116,0.4)] transition-colors" + /> +
+
+ + setForm({ ...form, email: e.target.value })} + required + className="w-full bg-[#12121a] border border-[rgba(255,255,255,0.08)] rounded-lg px-4 py-2.5 text-sm text-[#f5f5f7] focus:outline-none focus:border-[rgba(212,165,116,0.4)] transition-colors" + /> +
+
+
+ + setForm({ ...form, subject: e.target.value })} + className="w-full bg-[#12121a] border border-[rgba(255,255,255,0.08)] rounded-lg px-4 py-2.5 text-sm text-[#f5f5f7] focus:outline-none focus:border-[rgba(212,165,116,0.4)] transition-colors" + /> +
+
+ +