Implementación completa del sitio JAILGAMES (bloques A–J)
Sitio estático generado con Eleventy (11ty) + Nginx en Docker: - Plantillas Nunjucks con layout base, tarjetas y fichas individuales - Datos de juegos en YAML, colección ordenada por fecha - CSS con tema oscuro gaming y diseño responsive (3/2/1 columnas) - Lightbox vanilla JS para capturas de pantalla - Build multi-stage Docker (node:20-alpine → nginx:alpine) - 2 juegos de ejemplo con imágenes SVG placeholder Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@@ -0,0 +1,58 @@
|
||||
const yaml = require("js-yaml");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
module.exports = function (eleventyConfig) {
|
||||
// Parsear archivos YAML
|
||||
eleventyConfig.addDataExtension("yaml,yml", (contents) =>
|
||||
yaml.load(contents)
|
||||
);
|
||||
|
||||
// Copiar archivos estáticos al build
|
||||
// static/logo → _site/logo, static/games → _site/games
|
||||
eleventyConfig.addPassthroughCopy({ "static/logo": "logo" });
|
||||
eleventyConfig.addPassthroughCopy({ "static/games": "games" });
|
||||
eleventyConfig.addPassthroughCopy("downloads");
|
||||
eleventyConfig.addPassthroughCopy("src/css");
|
||||
eleventyConfig.addPassthroughCopy("src/js");
|
||||
|
||||
// Colección: todos los juegos ordenados por fecha
|
||||
eleventyConfig.addCollection("games", function () {
|
||||
const gamesDir = path.join(__dirname, "src", "games");
|
||||
const files = fs.readdirSync(gamesDir).filter((f) => f.endsWith(".yaml"));
|
||||
return files
|
||||
.map((f) => {
|
||||
const data = yaml.load(
|
||||
fs.readFileSync(path.join(gamesDir, f), "utf-8")
|
||||
);
|
||||
return data;
|
||||
})
|
||||
.sort((a, b) => new Date(b.release_date) - new Date(a.release_date));
|
||||
});
|
||||
|
||||
// Filtro para formatear fechas en español
|
||||
eleventyConfig.addFilter("dateFormat", function (dateStr) {
|
||||
const d = new Date(dateStr + "T00:00:00");
|
||||
return d.toLocaleDateString("es-ES", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
});
|
||||
|
||||
// Filtro Markdown para descripciones
|
||||
const markdownIt = require("markdown-it");
|
||||
const md = markdownIt({ html: true });
|
||||
eleventyConfig.addFilter("markdown", (content) => md.render(content || ""));
|
||||
|
||||
return {
|
||||
dir: {
|
||||
input: "src",
|
||||
includes: "_includes",
|
||||
data: "_data",
|
||||
output: "_site",
|
||||
},
|
||||
templateFormats: ["njk", "md"],
|
||||
htmlTemplateEngine: "njk",
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
# Build output
|
||||
_site/
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
@@ -0,0 +1,26 @@
|
||||
# ---------- ETAPA 1: Build ----------
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY src/ src/
|
||||
COPY static/ static/
|
||||
COPY .eleventy.js ./
|
||||
|
||||
RUN npx @11ty/eleventy
|
||||
|
||||
# ---------- ETAPA 2: Servir ----------
|
||||
FROM nginx:alpine
|
||||
|
||||
# Configuración personalizada de Nginx
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Copiar el sitio generado
|
||||
COPY --from=builder /app/_site /usr/share/nginx/html
|
||||
|
||||
# Copiar descargas (no pasan por Eleventy)
|
||||
COPY downloads/ /usr/share/nginx/html/downloads/
|
||||
|
||||
EXPOSE 80
|
||||
@@ -0,0 +1,925 @@
|
||||
# JAILGAMES — Plan Técnico Completo
|
||||
|
||||
---
|
||||
|
||||
## 1. Arquitectura General
|
||||
|
||||
### Decisión: Eleventy (11ty) como generador de sitio estático
|
||||
|
||||
Se descartan tres alternativas y se elige una:
|
||||
|
||||
| Opción | Ventajas | Inconvenientes |
|
||||
|---|---|---|
|
||||
| HTML/CSS/JS puro | Sin dependencias | Hay que duplicar HTML por cada juego; cualquier cambio en el layout obliga a tocar todos los archivos |
|
||||
| Astro / Vite | Moderno, componentes | Pesado para <10 juegos; requiere Node + build complejo |
|
||||
| Microframework (Flask, Express) | Dinámico | Necesita servidor con runtime activo; innecesario para contenido estático en ferias |
|
||||
| **Eleventy (11ty)** | **Plantillas Nunjucks, lee JSON/YAML nativamente, genera HTML puro, cero JS en el navegador** | **Requiere Node para el build (no en producción)** |
|
||||
|
||||
**Justificación concreta:**
|
||||
|
||||
- **Mantenimiento sencillo**: añadir un juego = crear un archivo YAML + copiar imágenes. Nada más.
|
||||
- **Cero dependencias en producción**: el resultado es HTML/CSS/JS estático servido por Nginx.
|
||||
- **Funciona sin internet**: perfecto para ferias presenciales. Se sirve desde un portátil con Docker.
|
||||
- **Nginx en lugar de Apache**: más ligero, configuración mínima para servir estáticos. Si se prefiere Apache, el cambio es trivial (se indica en la sección Docker).
|
||||
|
||||
---
|
||||
|
||||
## 2. Estructura de Carpetas
|
||||
|
||||
```
|
||||
jailgames/
|
||||
├── src/ # ← Fuentes (nunca se sirven directamente)
|
||||
│ ├── _data/
|
||||
│ │ └── site.json # Datos globales (nombre del sitio, redes, etc.)
|
||||
│ ├── _includes/
|
||||
│ │ ├── base.njk # Layout base HTML (head, header, footer)
|
||||
│ │ ├── game-card.njk # Componente: tarjeta de juego para la home
|
||||
│ │ └── game-page.njk # Layout: ficha individual de juego
|
||||
│ ├── games/ # Un archivo YAML por juego
|
||||
│ │ ├── cosmic-escape.yaml
|
||||
│ │ ├── pixel-dungeon.yaml
|
||||
│ │ └── ...
|
||||
│ ├── css/
|
||||
│ │ └── style.css # Estilos globales
|
||||
│ ├── js/
|
||||
│ │ └── main.js # JS mínimo (lightbox, filtros, etc.)
|
||||
│ ├── index.njk # Página principal (home)
|
||||
│ └── game.njk # Plantilla que genera cada /game/slug/
|
||||
│
|
||||
├── static/ # ← Archivos copiados tal cual al build
|
||||
│ ├── logo/
|
||||
│ │ └── jailgames-logo.png # Logo general del sitio
|
||||
│ ├── games/ # Carpeta por juego (nombre = slug)
|
||||
│ │ ├── cosmic-escape/
|
||||
│ │ │ ├── logo.png
|
||||
│ │ │ ├── screenshot-1.png
|
||||
│ │ │ ├── screenshot-2.png
|
||||
│ │ │ └── screenshot-3.png
|
||||
│ │ └── pixel-dungeon/
|
||||
│ │ ├── logo.png
|
||||
│ │ └── ...
|
||||
│ └── favicon.ico
|
||||
│
|
||||
├── downloads/ # ← Binarios descargables
|
||||
│ ├── cosmic-escape/
|
||||
│ │ ├── cosmic-escape-v1.2-windows.zip
|
||||
│ │ ├── cosmic-escape-v1.2-linux.tar.gz
|
||||
│ │ └── cosmic-escape-v1.2-mac.dmg
|
||||
│ └── pixel-dungeon/
|
||||
│ └── ...
|
||||
│
|
||||
├── _site/ # ← Salida del build (se sirve con Nginx)
|
||||
│
|
||||
├── .eleventy.js # Configuración de Eleventy
|
||||
├── package.json
|
||||
├── Dockerfile
|
||||
├── docker-compose.yml
|
||||
├── nginx.conf # Configuración de Nginx
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Resumen de ubicaciones
|
||||
|
||||
| Contenido | Ruta |
|
||||
|---|---|
|
||||
| Datos de cada juego | `src/games/<slug>.yaml` |
|
||||
| Imágenes de cada juego | `static/games/<slug>/` |
|
||||
| Binarios descargables | `downloads/<slug>/` |
|
||||
| Plantillas HTML | `src/_includes/` |
|
||||
| Página principal | `src/index.njk` |
|
||||
| Logo del sitio | `static/logo/jailgames-logo.png` |
|
||||
| CSS | `src/css/style.css` |
|
||||
| Salida final | `_site/` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Formato de Datos por Juego
|
||||
|
||||
Cada juego se define en un archivo YAML independiente dentro de `src/games/`. El nombre del archivo es el **slug** (identificador URL).
|
||||
|
||||
### Ejemplo: `src/games/cosmic-escape.yaml`
|
||||
|
||||
```yaml
|
||||
name: "Cosmic Escape"
|
||||
slug: "cosmic-escape"
|
||||
tagline: "Escapa de la estación antes de que colapse"
|
||||
description: |
|
||||
Un juego de plataformas 2D donde controlas a un astronauta atrapado en una
|
||||
estación espacial que se desmorona. Recoge oxígeno, esquiva meteoritos y
|
||||
encuentra la cápsula de escape antes de que se agote el tiempo.
|
||||
|
||||
Desarrollado en Godot Engine. Incluye 12 niveles y modo contrarreloj.
|
||||
|
||||
version: "1.2.0"
|
||||
release_date: "2025-11-15"
|
||||
tags:
|
||||
- plataformas
|
||||
- 2D
|
||||
- singleplayer
|
||||
|
||||
logo: "/games/cosmic-escape/logo.png"
|
||||
|
||||
screenshots:
|
||||
- "/games/cosmic-escape/screenshot-1.png"
|
||||
- "/games/cosmic-escape/screenshot-2.png"
|
||||
- "/games/cosmic-escape/screenshot-3.png"
|
||||
|
||||
downloads:
|
||||
- platform: "Windows"
|
||||
file: "/downloads/cosmic-escape/cosmic-escape-v1.2-windows.zip"
|
||||
size: "85 MB"
|
||||
- platform: "Linux"
|
||||
file: "/downloads/cosmic-escape/cosmic-escape-v1.2-linux.tar.gz"
|
||||
size: "78 MB"
|
||||
- platform: "macOS"
|
||||
file: "/downloads/cosmic-escape/cosmic-escape-v1.2-mac.dmg"
|
||||
size: "90 MB"
|
||||
|
||||
# Campos opcionales
|
||||
engine: "Godot 4.2"
|
||||
players: "1 jugador"
|
||||
genre: "Plataformas / Acción"
|
||||
repo: "https://gitea.local/jailgames/cosmic-escape" # Solo referencia interna
|
||||
```
|
||||
|
||||
### Campos obligatorios vs opcionales
|
||||
|
||||
| Campo | Obligatorio | Descripción |
|
||||
|---|---|---|
|
||||
| `name` | Sí | Nombre visible del juego |
|
||||
| `slug` | Sí | Identificador para URLs y carpetas |
|
||||
| `description` | Sí | Texto descriptivo (soporta párrafos) |
|
||||
| `version` | Sí | Versión del juego |
|
||||
| `release_date` | Sí | Fecha de publicación (YYYY-MM-DD) |
|
||||
| `downloads` | Sí | Al menos una entrada con platform/file/size |
|
||||
| `screenshots` | Sí | Al menos una captura |
|
||||
| `logo` | No | Si no existe, se usa una imagen placeholder |
|
||||
| `tagline` | No | Frase corta para la tarjeta de la home |
|
||||
| `tags` | No | Etiquetas para filtrar |
|
||||
| `engine` | No | Motor de desarrollo |
|
||||
| `players` | No | Número de jugadores |
|
||||
| `genre` | No | Género del juego |
|
||||
| `repo` | No | Enlace al repositorio (solo referencia) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Generación de Páginas
|
||||
|
||||
### 4.1 Configuración de Eleventy (`.eleventy.js`)
|
||||
|
||||
```javascript
|
||||
const yaml = require("js-yaml");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
module.exports = function (eleventyConfig) {
|
||||
// Parsear archivos YAML
|
||||
eleventyConfig.addDataExtension("yaml,yml", (contents) =>
|
||||
yaml.load(contents)
|
||||
);
|
||||
|
||||
// Copiar archivos estáticos al build
|
||||
eleventyConfig.addPassthroughCopy("static");
|
||||
eleventyConfig.addPassthroughCopy("downloads");
|
||||
eleventyConfig.addPassthroughCopy("src/css");
|
||||
eleventyConfig.addPassthroughCopy("src/js");
|
||||
|
||||
// Colección: todos los juegos ordenados por fecha
|
||||
eleventyConfig.addCollection("games", function () {
|
||||
const gamesDir = path.join(__dirname, "src", "games");
|
||||
const files = fs.readdirSync(gamesDir).filter((f) => f.endsWith(".yaml"));
|
||||
return files
|
||||
.map((f) => {
|
||||
const data = yaml.load(
|
||||
fs.readFileSync(path.join(gamesDir, f), "utf-8")
|
||||
);
|
||||
return data;
|
||||
})
|
||||
.sort((a, b) => new Date(b.release_date) - new Date(a.release_date));
|
||||
});
|
||||
|
||||
// Filtro para formatear fechas
|
||||
eleventyConfig.addFilter("dateFormat", function (dateStr) {
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleDateString("es-ES", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
});
|
||||
|
||||
// Filtro Markdown para descripciones
|
||||
const markdownIt = require("markdown-it");
|
||||
const md = markdownIt({ html: true });
|
||||
eleventyConfig.addFilter("markdown", (content) => md.render(content || ""));
|
||||
|
||||
return {
|
||||
dir: {
|
||||
input: "src",
|
||||
includes: "_includes",
|
||||
data: "_data",
|
||||
output: "_site",
|
||||
},
|
||||
templateFormats: ["njk", "md"],
|
||||
htmlTemplateEngine: "njk",
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 4.2 Página Principal (`src/index.njk`)
|
||||
|
||||
La home itera sobre la colección `games` y renderiza una tarjeta por cada juego.
|
||||
|
||||
```html
|
||||
---
|
||||
layout: base.njk
|
||||
title: "JAILGAMES — Nuestros Juegos"
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<img src="/logo/jailgames-logo.png" alt="JAILGAMES" class="site-logo" />
|
||||
<p class="hero-subtitle">Juegos caseros, hechos con cariño</p>
|
||||
</section>
|
||||
|
||||
<section class="games-grid">
|
||||
{% for game in collections.games %}
|
||||
{% include "game-card.njk" %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
```
|
||||
|
||||
### 4.3 Tarjeta de Juego (`src/_includes/game-card.njk`)
|
||||
|
||||
```html
|
||||
<a href="/game/{{ game.slug }}/" class="game-card">
|
||||
<div class="game-card__image">
|
||||
{% if game.logo %}
|
||||
<img src="{{ game.logo }}" alt="{{ game.name }} logo" loading="lazy" />
|
||||
{% else %}
|
||||
<div class="game-card__placeholder">🎮</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="game-card__info">
|
||||
<h2 class="game-card__title">{{ game.name }}</h2>
|
||||
{% if game.tagline %}
|
||||
<p class="game-card__tagline">{{ game.tagline }}</p>
|
||||
{% endif %}
|
||||
<div class="game-card__meta">
|
||||
<span class="game-card__version">v{{ game.version }}</span>
|
||||
{% if game.tags %}
|
||||
{% for tag in game.tags %}
|
||||
<span class="game-card__tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
```
|
||||
|
||||
### 4.4 Generación de Fichas Individuales (`src/game.njk`)
|
||||
|
||||
Eleventy genera una página por cada archivo YAML mediante **paginación**:
|
||||
|
||||
```html
|
||||
---
|
||||
pagination:
|
||||
data: collections.games
|
||||
size: 1
|
||||
alias: game
|
||||
permalink: "/game/{{ game.slug }}/"
|
||||
layout: game-page.njk
|
||||
eleventyComputed:
|
||||
title: "{{ game.name }} — JAILGAMES"
|
||||
---
|
||||
```
|
||||
|
||||
### 4.5 Layout de Ficha (`src/_includes/game-page.njk`)
|
||||
|
||||
```html
|
||||
{% extends "base.njk" %}
|
||||
|
||||
{% block content %}
|
||||
<article class="game-detail">
|
||||
|
||||
<header class="game-detail__header">
|
||||
{% if game.logo %}
|
||||
<img src="{{ game.logo }}" alt="{{ game.name }}" class="game-detail__logo" />
|
||||
{% endif %}
|
||||
<div>
|
||||
<h1>{{ game.name }}</h1>
|
||||
{% if game.tagline %}<p class="game-detail__tagline">{{ game.tagline }}</p>{% endif %}
|
||||
<div class="game-detail__meta">
|
||||
<span>v{{ game.version }}</span>
|
||||
<span>{{ game.release_date | dateFormat }}</span>
|
||||
{% if game.genre %}<span>{{ game.genre }}</span>{% endif %}
|
||||
{% if game.players %}<span>{{ game.players }}</span>{% endif %}
|
||||
{% if game.engine %}<span>{{ game.engine }}</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="game-detail__screenshots">
|
||||
{% for img in game.screenshots %}
|
||||
<img src="{{ img }}" alt="Captura de {{ game.name }}" loading="lazy"
|
||||
class="game-detail__screenshot" />
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<section class="game-detail__description">
|
||||
{{ game.description | markdown | safe }}
|
||||
</section>
|
||||
|
||||
<section class="game-detail__downloads">
|
||||
<h2>Descargar</h2>
|
||||
<div class="download-buttons">
|
||||
{% for dl in game.downloads %}
|
||||
<a href="{{ dl.file }}" class="download-btn" download>
|
||||
<span class="download-btn__platform">{{ dl.platform }}</span>
|
||||
<span class="download-btn__size">{{ dl.size }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if game.tags %}
|
||||
<section class="game-detail__tags">
|
||||
{% for tag in game.tags %}
|
||||
<span class="tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<a href="/" class="back-link">← Volver al catálogo</a>
|
||||
|
||||
</article>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### 4.6 Descargas Locales
|
||||
|
||||
Las descargas se enlazan directamente a archivos en `/downloads/<slug>/`. El atributo `download` en el `<a>` fuerza la descarga en lugar de abrir el archivo en el navegador. Nginx sirve estos archivos como estáticos. No hay enlaces a Gitea ni servicios externos.
|
||||
|
||||
---
|
||||
|
||||
## 5. Diseño y Estilo
|
||||
|
||||
### 5.1 Framework CSS: PicoCSS
|
||||
|
||||
Se recomienda **PicoCSS** (`@picocss/pico`) por:
|
||||
|
||||
- Estilo limpio sin necesidad de clases (aplica estilos directamente a elementos HTML semánticos).
|
||||
- Muy ligero (~10 KB).
|
||||
- Soporte nativo de tema oscuro/claro.
|
||||
- Responsive por defecto.
|
||||
- Sin dependencias de build (se puede usar como archivo CSS estático).
|
||||
|
||||
Si se prefiere más control visual, la alternativa es **Tailwind CSS** (requiere build step).
|
||||
|
||||
### 5.2 Esquema de Layout
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ HEADER │
|
||||
│ [Logo JAILGAMES] Juegos caseros... │
|
||||
├──────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────┐ ┌──────┐ ┌──────┐ │
|
||||
│ │ Logo │ │ Logo │ │ Logo │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ Name │ │ Name │ │ Name │ ← Grid │
|
||||
│ │ Tag │ │ Tag │ │ Tag │ 3 cols │
|
||||
│ └──────┘ └──────┘ └──────┘ │
|
||||
│ │
|
||||
│ ┌──────┐ ┌──────┐ │
|
||||
│ │ ... │ │ ... │ │
|
||||
│ └──────┘ └──────┘ │
|
||||
│ │
|
||||
├──────────────────────────────────────────────┤
|
||||
│ FOOTER │
|
||||
│ JAILGAMES · Hecho con ♥ │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Ficha individual:**
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ ← Volver HEADER │
|
||||
├──────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ [Logo] Nombre del Juego │
|
||||
│ v1.2.0 · 15 nov 2025 · Godot │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ Galería de capturas │ │
|
||||
│ │ [img1] [img2] [img3] │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Descripción del juego en prosa... │
|
||||
│ Párrafo 1... │
|
||||
│ Párrafo 2... │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Windows │ │ Linux │ │ macOS │ │
|
||||
│ │ 85 MB │ │ 78 MB │ │ 90 MB │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ │
|
||||
│ │
|
||||
│ #plataformas #2D #singleplayer │
|
||||
│ │
|
||||
├──────────────────────────────────────────────┤
|
||||
│ FOOTER │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5.3 Diseño Responsive
|
||||
|
||||
- **Desktop (>1024px)**: Grid de 3 columnas para tarjetas.
|
||||
- **Tablet (768–1024px)**: Grid de 2 columnas.
|
||||
- **Móvil (<768px)**: 1 columna, capturas en scroll horizontal.
|
||||
|
||||
### 5.4 Paleta de Colores Sugerida
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-bg: #0f0f0f;
|
||||
--color-surface: #1a1a2e;
|
||||
--color-primary: #e94560;
|
||||
--color-secondary: #16213e;
|
||||
--color-text: #eaeaea;
|
||||
--color-text-muted: #a0a0a0;
|
||||
--color-accent: #0f3460;
|
||||
--color-success: #4ecca3;
|
||||
--font-main: "Inter", "Segoe UI", sans-serif;
|
||||
--font-heading: "Space Grotesk", "Inter", sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
Tema oscuro por defecto (estética gaming), con contraste suficiente para legibilidad.
|
||||
|
||||
---
|
||||
|
||||
## 6. Pipeline de Actualización
|
||||
|
||||
### Añadir un juego nuevo
|
||||
|
||||
```
|
||||
Paso 1 Crear el archivo YAML
|
||||
→ src/games/<slug>.yaml
|
||||
(copiar de un juego existente y modificar campos)
|
||||
|
||||
Paso 2 Crear carpeta de imágenes
|
||||
→ static/games/<slug>/
|
||||
Copiar: logo.png, screenshot-1.png, screenshot-2.png, ...
|
||||
|
||||
Paso 3 Crear carpeta de descargas
|
||||
→ downloads/<slug>/
|
||||
Copiar binarios: <slug>-v<version>-<platform>.<ext>
|
||||
|
||||
Paso 4 Verificar que las rutas en el YAML coinciden con los archivos copiados.
|
||||
|
||||
Paso 5 Regenerar el sitio:
|
||||
$ npm run build
|
||||
(Esto ejecuta: npx @11ty/eleventy)
|
||||
|
||||
Paso 6 Verificar localmente:
|
||||
$ npm run serve
|
||||
(Esto ejecuta: npx @11ty/eleventy --serve)
|
||||
Abrir http://localhost:8080 y comprobar.
|
||||
|
||||
Paso 7 Reconstruir el contenedor Docker:
|
||||
$ docker compose up --build -d
|
||||
```
|
||||
|
||||
### Modificar un juego existente
|
||||
|
||||
```
|
||||
Paso 1 Editar src/games/<slug>.yaml
|
||||
Paso 2 Si hay imágenes nuevas, copiarlas a static/games/<slug>/
|
||||
Paso 3 Si hay binarios nuevos, copiarlos a downloads/<slug>/
|
||||
Paso 4 Regenerar: npm run build
|
||||
Paso 5 Reconstruir Docker: docker compose up --build -d
|
||||
```
|
||||
|
||||
### Diagrama del flujo
|
||||
|
||||
```
|
||||
[YAML nuevo] + [Imágenes] + [Binarios]
|
||||
│
|
||||
▼
|
||||
npm run build ← Eleventy lee YAML, genera HTML
|
||||
│
|
||||
▼
|
||||
_site/ ← HTML/CSS/JS estático listo
|
||||
│
|
||||
▼
|
||||
docker compose up ← Nginx sirve _site/ + downloads/
|
||||
│
|
||||
▼
|
||||
http://localhost ← Web lista para la feria
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Plan de Docker
|
||||
|
||||
### 7.1 Dockerfile
|
||||
|
||||
```dockerfile
|
||||
# ---------- ETAPA 1: Build ----------
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY src/ src/
|
||||
COPY static/ static/
|
||||
COPY .eleventy.js ./
|
||||
|
||||
RUN npx @11ty/eleventy
|
||||
|
||||
# ---------- ETAPA 2: Servir ----------
|
||||
FROM nginx:alpine
|
||||
|
||||
# Configuración personalizada de Nginx
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Copiar el sitio generado
|
||||
COPY --from=builder /app/_site /usr/share/nginx/html
|
||||
|
||||
# Copiar descargas (no pasan por Eleventy)
|
||||
COPY downloads/ /usr/share/nginx/html/downloads/
|
||||
|
||||
EXPOSE 80
|
||||
```
|
||||
|
||||
### 7.2 docker-compose.yml
|
||||
|
||||
```yaml
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
jailgames:
|
||||
build: .
|
||||
container_name: jailgames-web
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
# Volumen para descargas: permite actualizar binarios
|
||||
# sin reconstruir la imagen completa
|
||||
- ./downloads:/usr/share/nginx/html/downloads:ro
|
||||
# Volumen para imágenes de juegos (opcional, misma idea)
|
||||
- ./static/games:/usr/share/nginx/html/games:ro
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
### 7.3 nginx.conf
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Compresión para HTML/CSS/JS
|
||||
gzip on;
|
||||
gzip_types text/html text/css application/javascript application/json image/svg+xml;
|
||||
gzip_min_length 256;
|
||||
|
||||
# Cacheo agresivo para assets estáticos (feria local)
|
||||
location ~* \.(png|jpg|jpeg|gif|svg|ico|webp)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# Descargas: forzar descarga en lugar de abrir en navegador
|
||||
location /downloads/ {
|
||||
add_header Content-Disposition "attachment";
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
}
|
||||
|
||||
# SPA fallback (por si se accede directamente a /game/slug/)
|
||||
location / {
|
||||
try_files $uri $uri/ $uri.html =404;
|
||||
}
|
||||
|
||||
# Página 404 personalizada
|
||||
error_page 404 /404.html;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 Uso con Apache (alternativa)
|
||||
|
||||
Si se prefiere Apache, reemplazar la etapa 2 del Dockerfile:
|
||||
|
||||
```dockerfile
|
||||
FROM httpd:alpine
|
||||
COPY --from=builder /app/_site /usr/local/apache2/htdocs/
|
||||
COPY downloads/ /usr/local/apache2/htdocs/downloads/
|
||||
COPY apache.conf /usr/local/apache2/conf/httpd.conf
|
||||
EXPOSE 80
|
||||
```
|
||||
|
||||
### 7.5 Despliegue en Ferias sin Internet
|
||||
|
||||
```
|
||||
1. En la máquina de desarrollo (con internet):
|
||||
$ docker compose build
|
||||
$ docker save jailgames-web:latest | gzip > jailgames-image.tar.gz
|
||||
|
||||
2. Copiar jailgames-image.tar.gz al portátil de la feria (USB).
|
||||
|
||||
3. En el portátil de la feria (sin internet):
|
||||
$ gunzip -c jailgames-image.tar.gz | docker load
|
||||
$ docker run -d -p 80:80 \
|
||||
-v /ruta/local/downloads:/usr/share/nginx/html/downloads:ro \
|
||||
--name jailgames jailgames-web:latest
|
||||
|
||||
4. Abrir http://localhost en cualquier navegador.
|
||||
(O configurar un punto de acceso WiFi para que otros dispositivos
|
||||
se conecten a la IP local del portátil)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Lista de Tareas para Claude Code
|
||||
|
||||
Copiar esta lista tal cual y dársela a Claude Code como instrucciones paso a paso.
|
||||
|
||||
---
|
||||
|
||||
### BLOQUE A — Inicialización del Proyecto
|
||||
|
||||
```
|
||||
TAREA A1: Crear la estructura de carpetas del proyecto "jailgames"
|
||||
|
||||
Crear los siguientes directorios:
|
||||
- src/_data/
|
||||
- src/_includes/
|
||||
- src/games/
|
||||
- src/css/
|
||||
- src/js/
|
||||
- static/logo/
|
||||
- static/games/
|
||||
- downloads/
|
||||
|
||||
TAREA A2: Inicializar package.json
|
||||
|
||||
Ejecutar: npm init -y
|
||||
Instalar dependencias:
|
||||
npm install --save-dev @11ty/eleventy js-yaml markdown-it
|
||||
|
||||
Añadir scripts a package.json:
|
||||
"build": "npx @11ty/eleventy",
|
||||
"serve": "npx @11ty/eleventy --serve --port=8080",
|
||||
"clean": "rm -rf _site"
|
||||
```
|
||||
|
||||
### BLOQUE B — Configuración de Eleventy
|
||||
|
||||
```
|
||||
TAREA B1: Crear .eleventy.js en la raíz del proyecto
|
||||
|
||||
Contenido: Configuración de Eleventy que:
|
||||
- Parsea archivos YAML con js-yaml
|
||||
- Copia "static" y "downloads" como passthrough
|
||||
- Copia "src/css" y "src/js" como passthrough
|
||||
- Crea una colección "games" leyendo todos los .yaml de src/games/
|
||||
- Ordena los juegos por release_date (más recientes primero)
|
||||
- Añade filtro "dateFormat" que formatea fechas en español
|
||||
- Añade filtro "markdown" que convierte texto a HTML con markdown-it
|
||||
- Input: src/, Output: _site/, Includes: _includes/, Data: _data/
|
||||
|
||||
TAREA B2: Crear src/_data/site.json
|
||||
|
||||
Contenido:
|
||||
{
|
||||
"name": "JAILGAMES",
|
||||
"tagline": "Juegos caseros, hechos con cariño",
|
||||
"url": "http://localhost",
|
||||
"lang": "es"
|
||||
}
|
||||
```
|
||||
|
||||
### BLOQUE C — Plantillas HTML (Nunjucks)
|
||||
|
||||
```
|
||||
TAREA C1: Crear src/_includes/base.njk
|
||||
|
||||
Layout base HTML5 que incluya:
|
||||
- DOCTYPE html, lang="es"
|
||||
- Meta charset, viewport
|
||||
- Title dinámico: {{ title }} o fallback a site.name
|
||||
- Enlace a /css/style.css
|
||||
- Header con logo de JAILGAMES (enlazado a /)
|
||||
- Bloque {% block content %}{% endblock %}
|
||||
- Footer con "JAILGAMES · Hecho con ♥"
|
||||
- Enlace a /js/main.js antes de cerrar body
|
||||
|
||||
TAREA C2: Crear src/_includes/game-card.njk
|
||||
|
||||
Componente tarjeta de juego:
|
||||
- Enlace <a> a /game/{{ game.slug }}/
|
||||
- Imagen del logo del juego (o placeholder si no hay logo)
|
||||
- Nombre del juego <h2>
|
||||
- Tagline si existe
|
||||
- Versión y tags en un div de metadatos
|
||||
|
||||
TAREA C3: Crear src/_includes/game-page.njk
|
||||
|
||||
Layout para ficha individual. Extiende base.njk. Incluye:
|
||||
- Header con logo del juego + nombre + tagline + metadatos
|
||||
(versión, fecha formateada, género, players, engine)
|
||||
- Sección de galería de capturas (iterar screenshots)
|
||||
- Sección de descripción (usar filtro markdown)
|
||||
- Sección de descargas: un botón por cada entrada en downloads
|
||||
(con atributo download, mostrar plataforma y tamaño)
|
||||
- Sección de tags
|
||||
- Enlace "← Volver al catálogo" a /
|
||||
|
||||
TAREA C4: Crear src/index.njk
|
||||
|
||||
Página principal:
|
||||
- Layout: base.njk
|
||||
- Sección hero con logo y subtítulo
|
||||
- Sección games-grid que itera collections.games
|
||||
e incluye game-card.njk para cada juego
|
||||
|
||||
TAREA C5: Crear src/game.njk
|
||||
|
||||
Plantilla de paginación que genera /game/<slug>/ para cada juego:
|
||||
- Front matter con pagination (data: collections.games, size: 1, alias: game)
|
||||
- Permalink: /game/{{ game.slug }}/
|
||||
- Layout: game-page.njk
|
||||
- eleventyComputed title
|
||||
```
|
||||
|
||||
### BLOQUE D — Estilos CSS
|
||||
|
||||
```
|
||||
TAREA D1: Crear src/css/style.css
|
||||
|
||||
Archivo CSS completo que incluya:
|
||||
- Variables CSS (colores tema oscuro gaming, fuentes)
|
||||
- Reset mínimo (box-sizing, margin 0)
|
||||
- Body: fondo oscuro, color claro, fuente sans-serif
|
||||
- .site-header: flex, centrado, con logo a la izquierda
|
||||
- .site-logo: max-height 60px
|
||||
- .hero: centrado, padding generoso
|
||||
- .hero-subtitle: color muted
|
||||
- .games-grid: CSS Grid, 3 columnas en desktop, 2 en tablet, 1 en móvil
|
||||
- .game-card: bloque con fondo surface, borde redondeado, hover con sombra,
|
||||
transición suave. Sin text-decoration.
|
||||
- .game-card__image img: object-fit contain, altura fija
|
||||
- .game-card__title: color blanco
|
||||
- .game-card__tagline: color muted, font-size pequeño
|
||||
- .game-card__tag: badge pequeño con color accent
|
||||
- .game-detail: max-width 900px, centrado, padding
|
||||
- .game-detail__header: flex, logo a la izquierda, info a la derecha
|
||||
- .game-detail__screenshots: grid o flex con scroll horizontal en móvil
|
||||
- .game-detail__screenshot: borde redondeado, max-width 100%
|
||||
- .download-buttons: flex, wrap
|
||||
- .download-btn: botón grande con color primary, hover,
|
||||
icono de plataforma si es posible
|
||||
- .tag: badge igual que game-card__tag
|
||||
- .back-link: estilo enlace simple
|
||||
- .site-footer: centrado, padding, color muted
|
||||
- Media queries para responsive (768px, 1024px)
|
||||
```
|
||||
|
||||
### BLOQUE E — JavaScript Mínimo
|
||||
|
||||
```
|
||||
TAREA E1: Crear src/js/main.js
|
||||
|
||||
Funcionalidad mínima:
|
||||
- Lightbox simple para capturas: al hacer clic en una screenshot,
|
||||
mostrarla en overlay a pantalla completa. Clic en overlay = cerrar.
|
||||
- (Opcional) Filtrado de juegos por tags en la home.
|
||||
- No usar frameworks, vanilla JS puro.
|
||||
```
|
||||
|
||||
### BLOQUE F — Datos de Ejemplo
|
||||
|
||||
```
|
||||
TAREA F1: Crear 2 archivos YAML de ejemplo en src/games/
|
||||
|
||||
Archivo 1: src/games/cosmic-escape.yaml
|
||||
- Rellenar con datos ficticios pero realistas
|
||||
- Incluir al menos 3 screenshots, 2 plataformas de descarga
|
||||
- Incluir todos los campos opcionales como ejemplo
|
||||
|
||||
Archivo 2: src/games/pixel-dungeon.yaml
|
||||
- Rellenar con datos ficticios diferentes
|
||||
- Incluir solo campos obligatorios (ejemplo minimalista)
|
||||
|
||||
TAREA F2: Crear imágenes placeholder
|
||||
|
||||
- static/logo/jailgames-logo.png → placeholder SVG o imagen genérica
|
||||
- static/games/cosmic-escape/logo.png → placeholder
|
||||
- static/games/cosmic-escape/screenshot-1.png → placeholder 800x450
|
||||
- static/games/cosmic-escape/screenshot-2.png → placeholder 800x450
|
||||
- static/games/pixel-dungeon/logo.png → placeholder
|
||||
|
||||
(Pueden ser SVGs generados con texto identificativo)
|
||||
|
||||
TAREA F3: Crear archivos de descarga de ejemplo
|
||||
|
||||
- downloads/cosmic-escape/README.txt
|
||||
(archivo de texto que diga "Aquí irá el binario de Cosmic Escape")
|
||||
- downloads/pixel-dungeon/README.txt
|
||||
(archivo de texto que diga "Aquí irá el binario de Pixel Dungeon")
|
||||
```
|
||||
|
||||
### BLOQUE G — Página 404
|
||||
|
||||
```
|
||||
TAREA G1: Crear src/404.njk
|
||||
|
||||
Página de error 404:
|
||||
- Layout: base.njk
|
||||
- Permalink: /404.html
|
||||
- Mensaje amigable: "Página no encontrada"
|
||||
- Enlace de vuelta a la home
|
||||
```
|
||||
|
||||
### BLOQUE H — Docker
|
||||
|
||||
```
|
||||
TAREA H1: Crear Dockerfile en la raíz
|
||||
|
||||
Multi-stage build:
|
||||
- Etapa 1 (builder): node:20-alpine, npm ci, npx @11ty/eleventy
|
||||
- Etapa 2 (serve): nginx:alpine, copiar _site y downloads, copiar nginx.conf
|
||||
|
||||
TAREA H2: Crear nginx.conf en la raíz
|
||||
|
||||
Configuración Nginx:
|
||||
- Puerto 80, root /usr/share/nginx/html
|
||||
- Gzip activado para text/html, text/css, application/javascript
|
||||
- Cache de imágenes 7 días
|
||||
- Descargas con Content-Disposition: attachment
|
||||
- try_files para rutas limpias
|
||||
- error_page 404 → /404.html
|
||||
|
||||
TAREA H3: Crear docker-compose.yml en la raíz
|
||||
|
||||
Servicio jailgames:
|
||||
- Build desde el directorio actual
|
||||
- Puerto 80:80
|
||||
- Volumen downloads como read-only
|
||||
- restart: unless-stopped
|
||||
```
|
||||
|
||||
### BLOQUE I — Documentación
|
||||
|
||||
```
|
||||
TAREA I1: Crear README.md en la raíz
|
||||
|
||||
Incluir:
|
||||
- Nombre del proyecto y descripción breve
|
||||
- Requisitos previos (Node.js 18+, Docker)
|
||||
- Cómo instalar dependencias (npm ci)
|
||||
- Cómo ejecutar en desarrollo (npm run serve)
|
||||
- Cómo hacer build (npm run build)
|
||||
- Cómo añadir un juego nuevo (los 7 pasos del pipeline)
|
||||
- Cómo desplegar con Docker (docker compose up --build)
|
||||
- Cómo desplegar sin internet (docker save/load)
|
||||
- Estructura de carpetas resumida
|
||||
```
|
||||
|
||||
### BLOQUE J — Verificación Final
|
||||
|
||||
```
|
||||
TAREA J1: Ejecutar npm run build y verificar que _site/ se genera correctamente.
|
||||
|
||||
TAREA J2: Ejecutar npm run serve y verificar en http://localhost:8080:
|
||||
- La home muestra las tarjetas de los 2 juegos de ejemplo
|
||||
- Hacer clic en una tarjeta lleva a la ficha individual
|
||||
- La ficha muestra logo, capturas, descripción y botones de descarga
|
||||
- Los botones de descarga funcionan (descargan el archivo)
|
||||
- El enlace "Volver" funciona
|
||||
- La página 404 funciona al visitar una ruta inexistente
|
||||
- El diseño es responsive (probar en móvil con DevTools)
|
||||
|
||||
TAREA J3: Ejecutar docker compose up --build y verificar en http://localhost:80
|
||||
que todo funciona igual que en J2.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resumen Ejecutivo
|
||||
|
||||
| Aspecto | Decisión |
|
||||
|---|---|
|
||||
| Generador | Eleventy (11ty) |
|
||||
| Formato de datos | YAML (1 archivo por juego) |
|
||||
| Plantillas | Nunjucks |
|
||||
| CSS | CSS custom con tema oscuro (opcionalmente PicoCSS) |
|
||||
| Servidor | Nginx en Docker |
|
||||
| Build | Multi-stage Docker (Node → Nginx) |
|
||||
| Tiempo para añadir un juego | ~5 minutos (crear YAML + copiar archivos + rebuild) |
|
||||
| Funciona sin internet | Sí (docker save/load) |
|
||||
@@ -0,0 +1,151 @@
|
||||
# JAILGAMES
|
||||
|
||||
Sitio web estático para publicar y descargar juegos indie del estudio JAILGAMES.
|
||||
Generado con **Eleventy (11ty)** y servido con **Nginx en Docker**.
|
||||
|
||||
---
|
||||
|
||||
## Requisitos previos
|
||||
|
||||
- **Node.js 18+** (para el build local)
|
||||
- **Docker** + **Docker Compose** (para despliegue)
|
||||
|
||||
---
|
||||
|
||||
## Instalación
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Desarrollo local
|
||||
|
||||
```bash
|
||||
npm run serve
|
||||
```
|
||||
|
||||
Abre [http://localhost:8080](http://localhost:8080) en el navegador.
|
||||
Eleventy reconstruye automáticamente al guardar cambios.
|
||||
|
||||
---
|
||||
|
||||
## Build estático
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
El resultado queda en `_site/`. Para limpiar:
|
||||
|
||||
```bash
|
||||
npm run clean
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Añadir un juego nuevo
|
||||
|
||||
1. **Crear el archivo YAML** del juego:
|
||||
```
|
||||
src/games/<slug>.yaml
|
||||
```
|
||||
Copiar de un juego existente y modificar los campos. El `slug` es el identificador URL (solo letras minúsculas, números y guiones).
|
||||
|
||||
2. **Crear la carpeta de imágenes:**
|
||||
```
|
||||
static/games/<slug>/
|
||||
```
|
||||
Copiar: `logo.png` (o `.svg`), `screenshot-1.png`, `screenshot-2.png`, ...
|
||||
|
||||
3. **Crear la carpeta de descargas:**
|
||||
```
|
||||
downloads/<slug>/
|
||||
```
|
||||
Copiar los binarios: `<slug>-v<version>-<plataforma>.<ext>`
|
||||
|
||||
4. **Verificar** que las rutas en el YAML coinciden exactamente con los archivos copiados.
|
||||
|
||||
5. **Regenerar el sitio:**
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
6. **Verificar localmente:**
|
||||
```bash
|
||||
npm run serve
|
||||
```
|
||||
Abrir [http://localhost:8080](http://localhost:8080) y comprobar el juego nuevo.
|
||||
|
||||
7. **Reconstruir el contenedor Docker:**
|
||||
```bash
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Despliegue con Docker
|
||||
|
||||
```bash
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
Acceder en [http://localhost](http://localhost).
|
||||
|
||||
Para detener:
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Despliegue sin internet (ferias)
|
||||
|
||||
**En la máquina con internet:**
|
||||
```bash
|
||||
docker compose build
|
||||
docker save jailgames-web:latest | gzip > jailgames-image.tar.gz
|
||||
```
|
||||
|
||||
**Copiar** `jailgames-image.tar.gz` al portátil de la feria (USB).
|
||||
|
||||
**En el portátil de la feria (sin internet):**
|
||||
```bash
|
||||
gunzip -c jailgames-image.tar.gz | docker load
|
||||
docker run -d -p 80:80 \
|
||||
-v /ruta/local/downloads:/usr/share/nginx/html/downloads:ro \
|
||||
--name jailgames jailgames-web:latest
|
||||
```
|
||||
|
||||
Abrir [http://localhost](http://localhost) en cualquier navegador.
|
||||
|
||||
---
|
||||
|
||||
## Estructura de carpetas
|
||||
|
||||
```
|
||||
jailgames/
|
||||
├── src/
|
||||
│ ├── _data/site.json # Datos globales del sitio
|
||||
│ ├── _includes/
|
||||
│ │ ├── base.njk # Layout base HTML
|
||||
│ │ ├── game-card.njk # Tarjeta de juego (home)
|
||||
│ │ └── game-page.njk # Ficha individual de juego
|
||||
│ ├── games/ # Un archivo YAML por juego
|
||||
│ ├── css/style.css # Estilos globales
|
||||
│ ├── js/main.js # JS mínimo (lightbox)
|
||||
│ ├── index.njk # Página principal
|
||||
│ ├── game.njk # Generador de fichas individuales
|
||||
│ └── 404.njk # Página de error 404
|
||||
├── static/
|
||||
│ ├── logo/ # Logo del sitio
|
||||
│ └── games/<slug>/ # Imágenes de cada juego
|
||||
├── downloads/<slug>/ # Binarios descargables
|
||||
├── _site/ # Build generado (no versionar)
|
||||
├── .eleventy.js # Configuración de Eleventy
|
||||
├── package.json
|
||||
├── Dockerfile
|
||||
├── docker-compose.yml
|
||||
└── nginx.conf
|
||||
```
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
jailgames:
|
||||
build: .
|
||||
container_name: jailgames-web
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
# Permite actualizar binarios sin reconstruir la imagen completa
|
||||
- ./downloads:/usr/share/nginx/html/downloads:ro
|
||||
# Permite actualizar imágenes de juegos sin reconstruir
|
||||
- ./static/games:/usr/share/nginx/html/games:ro
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,15 @@
|
||||
Aquí irá el binario de Cosmic Escape.
|
||||
|
||||
Nombre del juego: Cosmic Escape
|
||||
Versión: 1.2.0
|
||||
Motor: Godot 4.2
|
||||
|
||||
Plataformas disponibles:
|
||||
- Windows: cosmic-escape-v1.2-windows.zip (~85 MB)
|
||||
- Linux: cosmic-escape-v1.2-linux.tar.gz (~78 MB)
|
||||
- macOS: cosmic-escape-v1.2-mac.dmg (~90 MB)
|
||||
|
||||
Instrucciones de instalación:
|
||||
Windows: Descomprimir el ZIP y ejecutar CosmiEscape.exe
|
||||
Linux: Descomprimir el tar.gz y ejecutar ./cosmic-escape
|
||||
macOS: Abrir el DMG y arrastrar la app a Aplicaciones
|
||||
@@ -0,0 +1,10 @@
|
||||
Aquí irá el binario de Pixel Dungeon.
|
||||
|
||||
Nombre del juego: Pixel Dungeon
|
||||
Versión: 0.9.1
|
||||
|
||||
Plataformas disponibles:
|
||||
- Windows: pixel-dungeon-v0.9.1-windows.zip (~22 MB)
|
||||
|
||||
Instrucciones de instalación:
|
||||
Windows: Descomprimir el ZIP y ejecutar PixelDungeon.exe
|
||||
@@ -0,0 +1,33 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Compresión para HTML/CSS/JS
|
||||
gzip on;
|
||||
gzip_types text/html text/css application/javascript application/json image/svg+xml;
|
||||
gzip_min_length 256;
|
||||
|
||||
# Cacheo para assets estáticos (feria local)
|
||||
location ~* \.(png|jpg|jpeg|gif|svg|ico|webp)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# Descargas: forzar descarga en lugar de abrir en navegador
|
||||
location /downloads/ {
|
||||
add_header Content-Disposition "attachment";
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
}
|
||||
|
||||
# Rutas limpias
|
||||
location / {
|
||||
try_files $uri $uri/ $uri.html =404;
|
||||
}
|
||||
|
||||
# Página 404 personalizada
|
||||
error_page 404 /404.html;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "web_jailgames",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "npx @11ty/eleventy",
|
||||
"serve": "npx @11ty/eleventy --serve --port=8080",
|
||||
"clean": "rm -rf _site"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitea.sustancia.synology.me/jaildesigner-utilitats/web_jailgames.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@11ty/eleventy": "^3.1.5",
|
||||
"js-yaml": "^4.1.1",
|
||||
"markdown-it": "^14.1.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
layout: base.njk
|
||||
title: "404 — Página no encontrada — JAILGAMES"
|
||||
permalink: /404.html
|
||||
---
|
||||
|
||||
<div class="not-found">
|
||||
<h1>404</h1>
|
||||
<h2>Página no encontrada</h2>
|
||||
<p>La página que buscas no existe o ha sido movida.</p>
|
||||
<a href="/" class="download-btn" style="display:inline-flex;">← Volver al catálogo</a>
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "JAILGAMES",
|
||||
"tagline": "Juegos caseros, hechos con cariño",
|
||||
"url": "http://localhost",
|
||||
"lang": "es"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ site.lang | default('es') }}">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{ title if title else site.name }}</title>
|
||||
<link rel="stylesheet" href="/css/style.css" />
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="site-header">
|
||||
<a href="/" class="site-header__logo-link">
|
||||
<img src="/logo/jailgames-logo.svg" alt="{{ site.name }}" class="site-logo" />
|
||||
<span class="site-header__name">{{ site.name }}</span>
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<main class="site-main">
|
||||
{{ content | safe }}
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<p>{{ site.name }} · Hecho con ♥</p>
|
||||
</footer>
|
||||
|
||||
<script src="/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<a href="/game/{{ game.slug }}/" class="game-card">
|
||||
<div class="game-card__image">
|
||||
{% if game.logo %}
|
||||
<img src="{{ game.logo }}" alt="{{ game.name }} logo" loading="lazy" />
|
||||
{% else %}
|
||||
<div class="game-card__placeholder">🎮</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="game-card__info">
|
||||
<h2 class="game-card__title">{{ game.name }}</h2>
|
||||
{% if game.tagline %}
|
||||
<p class="game-card__tagline">{{ game.tagline }}</p>
|
||||
{% endif %}
|
||||
<div class="game-card__meta">
|
||||
<span class="game-card__version">v{{ game.version }}</span>
|
||||
{% if game.tags %}
|
||||
{% for tag in game.tags %}
|
||||
<span class="game-card__tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
layout: base.njk
|
||||
---
|
||||
<article class="game-detail">
|
||||
|
||||
<header class="game-detail__header">
|
||||
{% if game.logo %}
|
||||
<img src="{{ game.logo }}" alt="{{ game.name }}" class="game-detail__logo" />
|
||||
{% endif %}
|
||||
<div class="game-detail__header-info">
|
||||
<h1>{{ game.name }}</h1>
|
||||
{% if game.tagline %}
|
||||
<p class="game-detail__tagline">{{ game.tagline }}</p>
|
||||
{% endif %}
|
||||
<div class="game-detail__meta">
|
||||
<span>v{{ game.version }}</span>
|
||||
<span>{{ game.release_date | dateFormat }}</span>
|
||||
{% if game.genre %}<span>{{ game.genre }}</span>{% endif %}
|
||||
{% if game.players %}<span>{{ game.players }}</span>{% endif %}
|
||||
{% if game.engine %}<span>{{ game.engine }}</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if game.screenshots and game.screenshots.length %}
|
||||
<section class="game-detail__screenshots">
|
||||
{% for img in game.screenshots %}
|
||||
<img src="{{ img }}" alt="Captura {{ loop.index }} de {{ game.name }}"
|
||||
loading="lazy" class="game-detail__screenshot" />
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="game-detail__description">
|
||||
{{ game.description | markdown | safe }}
|
||||
</section>
|
||||
|
||||
<section class="game-detail__downloads">
|
||||
<h2>Descargar</h2>
|
||||
<div class="download-buttons">
|
||||
{% for dl in game.downloads %}
|
||||
<a href="{{ dl.file }}" class="download-btn" download>
|
||||
<span class="download-btn__platform">{{ dl.platform }}</span>
|
||||
<span class="download-btn__size">{{ dl.size }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if game.tags %}
|
||||
<section class="game-detail__tags">
|
||||
{% for tag in game.tags %}
|
||||
<span class="tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<a href="/" class="back-link">← Volver al catálogo</a>
|
||||
|
||||
</article>
|
||||
@@ -0,0 +1,531 @@
|
||||
/* =============================================
|
||||
VARIABLES — Tema oscuro gaming
|
||||
============================================= */
|
||||
:root {
|
||||
--color-bg: #0f0f0f;
|
||||
--color-surface: #1a1a2e;
|
||||
--color-surface-hover: #22223a;
|
||||
--color-primary: #e94560;
|
||||
--color-primary-hover: #c73650;
|
||||
--color-secondary: #16213e;
|
||||
--color-text: #eaeaea;
|
||||
--color-text-muted: #a0a0a0;
|
||||
--color-accent: #0f3460;
|
||||
--color-success: #4ecca3;
|
||||
--color-border: #2a2a4a;
|
||||
--font-main: "Segoe UI", "Inter", system-ui, sans-serif;
|
||||
--font-heading: "Segoe UI", "Inter", system-ui, sans-serif;
|
||||
--radius: 8px;
|
||||
--radius-lg: 12px;
|
||||
--shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
--shadow-hover: 0 8px 32px rgba(233, 69, 96, 0.3);
|
||||
--transition: 0.2s ease;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
RESET MÍNIMO
|
||||
============================================= */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
BASE
|
||||
============================================= */
|
||||
body {
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-main);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
font-family: var(--font-heading);
|
||||
margin: 0 0 0.5em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
HEADER
|
||||
============================================= */
|
||||
.site-header {
|
||||
background-color: var(--color-secondary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: 0.75rem 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.site-header__logo-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
text-decoration: none;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.site-logo {
|
||||
max-height: 48px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.site-header__name {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
MAIN
|
||||
============================================= */
|
||||
.site-main {
|
||||
flex: 1;
|
||||
padding: 2rem 1.5rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
HERO
|
||||
============================================= */
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem 2rem;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
max-height: 120px;
|
||||
margin: 0 auto 1.5rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
GAMES GRID
|
||||
============================================= */
|
||||
.games-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
GAME CARD
|
||||
============================================= */
|
||||
.game-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
color: var(--color-text);
|
||||
transition: transform var(--transition), box-shadow var(--transition), border-color var(--transition);
|
||||
}
|
||||
|
||||
.game-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-hover);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.game-card__image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-color: var(--color-accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.game-card__image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.game-card__placeholder {
|
||||
font-size: 4rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.game-card__info {
|
||||
padding: 1rem 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.game-card__title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.game-card__tagline {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.game-card__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-top: auto;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.game-card__version {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-success);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.game-card__tag {
|
||||
font-size: 0.7rem;
|
||||
background-color: var(--color-accent);
|
||||
color: var(--color-text);
|
||||
border-radius: 4px;
|
||||
padding: 0.15em 0.5em;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
GAME DETAIL
|
||||
============================================= */
|
||||
.game-detail {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.game-detail__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.game-detail__logo {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: contain;
|
||||
border-radius: var(--radius);
|
||||
background-color: var(--color-accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.game-detail__header-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.game-detail__header-info h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.game-detail__tagline {
|
||||
font-size: 1rem;
|
||||
color: var(--color-text-muted);
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
.game-detail__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.game-detail__meta span {
|
||||
font-size: 0.8rem;
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
padding: 0.2em 0.6em;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
SCREENSHOTS
|
||||
============================================= */
|
||||
.game-detail__screenshots {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 0.5rem;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-border) transparent;
|
||||
}
|
||||
|
||||
.game-detail__screenshot {
|
||||
width: 320px;
|
||||
min-width: 280px;
|
||||
height: 180px;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
transition: transform var(--transition), box-shadow var(--transition);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.game-detail__screenshot:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
DESCRIPCIÓN
|
||||
============================================= */
|
||||
.game-detail__description {
|
||||
margin-bottom: 2rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.game-detail__description p {
|
||||
margin: 0 0 1em;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
DESCARGAS
|
||||
============================================= */
|
||||
.game-detail__downloads {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.game-detail__downloads h2 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.download-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background-color: var(--color-primary);
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
border-radius: var(--radius);
|
||||
font-weight: 600;
|
||||
transition: background-color var(--transition), transform var(--transition);
|
||||
min-width: 120px;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.download-btn:hover {
|
||||
background-color: var(--color-primary-hover);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.download-btn__platform {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.download-btn__size {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.85;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
TAGS
|
||||
============================================= */
|
||||
.game-detail__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 0.8rem;
|
||||
background-color: var(--color-accent);
|
||||
color: var(--color-text);
|
||||
border-radius: 4px;
|
||||
padding: 0.25em 0.7em;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
BACK LINK
|
||||
============================================= */
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-top: 1rem;
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
transition: color var(--transition);
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
FOOTER
|
||||
============================================= */
|
||||
.site-footer {
|
||||
background-color: var(--color-secondary);
|
||||
border-top: 1px solid var(--color-border);
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
404
|
||||
============================================= */
|
||||
.not-found {
|
||||
text-align: center;
|
||||
padding: 4rem 1rem;
|
||||
}
|
||||
|
||||
.not-found h1 {
|
||||
font-size: 5rem;
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.not-found p {
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
LIGHTBOX
|
||||
============================================= */
|
||||
.lightbox-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.92);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
cursor: zoom-out;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.lightbox-overlay img {
|
||||
max-width: 95vw;
|
||||
max-height: 90vh;
|
||||
object-fit: contain;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.lightbox-close {
|
||||
position: absolute;
|
||||
top: 1.5rem;
|
||||
right: 1.5rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
padding: 0.25rem 0.5rem;
|
||||
opacity: 0.8;
|
||||
transition: opacity var(--transition);
|
||||
}
|
||||
|
||||
.lightbox-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
RESPONSIVE — Tablet (≤ 1024px)
|
||||
============================================= */
|
||||
@media (max-width: 1024px) {
|
||||
.games-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
RESPONSIVE — Móvil (≤ 768px)
|
||||
============================================= */
|
||||
@media (max-width: 768px) {
|
||||
.site-main {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.games-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.game-detail__header {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.game-detail__meta {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.game-detail__header-info h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.download-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
max-height: 80px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
pagination:
|
||||
data: collections.games
|
||||
size: 1
|
||||
alias: game
|
||||
permalink: "/game/{{ game.slug }}/"
|
||||
layout: game-page.njk
|
||||
eleventyComputed:
|
||||
title: "{{ game.name }} — JAILGAMES"
|
||||
---
|
||||
@@ -0,0 +1,40 @@
|
||||
name: "Cosmic Escape"
|
||||
slug: "cosmic-escape"
|
||||
tagline: "Escapa de la estación antes de que colapse"
|
||||
description: |
|
||||
Un juego de plataformas 2D donde controlas a un astronauta atrapado en una
|
||||
estación espacial que se desmorona. Recoge oxígeno, esquiva meteoritos y
|
||||
encuentra la cápsula de escape antes de que se agote el tiempo.
|
||||
|
||||
Desarrollado en Godot Engine. Incluye 12 niveles y modo contrarreloj.
|
||||
Ideal para partidas cortas e intensas.
|
||||
|
||||
version: "1.2.0"
|
||||
release_date: "2025-11-15"
|
||||
tags:
|
||||
- plataformas
|
||||
- 2D
|
||||
- singleplayer
|
||||
|
||||
logo: "/games/cosmic-escape/logo.svg"
|
||||
|
||||
screenshots:
|
||||
- "/games/cosmic-escape/screenshot-1.svg"
|
||||
- "/games/cosmic-escape/screenshot-2.svg"
|
||||
- "/games/cosmic-escape/screenshot-3.svg"
|
||||
|
||||
downloads:
|
||||
- platform: "Windows"
|
||||
file: "/downloads/cosmic-escape/README.txt"
|
||||
size: "85 MB"
|
||||
- platform: "Linux"
|
||||
file: "/downloads/cosmic-escape/README.txt"
|
||||
size: "78 MB"
|
||||
- platform: "macOS"
|
||||
file: "/downloads/cosmic-escape/README.txt"
|
||||
size: "90 MB"
|
||||
|
||||
engine: "Godot 4.2"
|
||||
players: "1 jugador"
|
||||
genre: "Plataformas / Acción"
|
||||
repo: "https://gitea.local/jailgames/cosmic-escape"
|
||||
@@ -0,0 +1,23 @@
|
||||
name: "Pixel Dungeon"
|
||||
slug: "pixel-dungeon"
|
||||
description: |
|
||||
Un roguelike de mazmorras con gráficos pixel art de 16x16. Explora
|
||||
generaciones procedurales de niveles, recoge objetos y derrota al jefe
|
||||
final. Cada partida es única.
|
||||
|
||||
version: "0.9.1"
|
||||
release_date: "2025-08-03"
|
||||
tags:
|
||||
- roguelike
|
||||
- pixel-art
|
||||
- singleplayer
|
||||
|
||||
logo: "/games/pixel-dungeon/logo.svg"
|
||||
|
||||
screenshots:
|
||||
- "/games/pixel-dungeon/screenshot-1.svg"
|
||||
|
||||
downloads:
|
||||
- platform: "Windows"
|
||||
file: "/downloads/pixel-dungeon/README.txt"
|
||||
size: "22 MB"
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
layout: base.njk
|
||||
title: "JAILGAMES — Nuestros Juegos"
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<img src="/logo/jailgames-logo.svg" alt="JAILGAMES" class="site-logo hero-logo" />
|
||||
<p class="hero-subtitle">{{ site.tagline }}</p>
|
||||
</section>
|
||||
|
||||
<section class="games-grid">
|
||||
{% for game in collections.games %}
|
||||
{% include "game-card.njk" %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
@@ -0,0 +1,59 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// ── Lightbox para capturas de pantalla ──────────────────────────────────
|
||||
function initLightbox() {
|
||||
const screenshots = document.querySelectorAll(".game-detail__screenshot");
|
||||
if (!screenshots.length) return;
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "lightbox-overlay";
|
||||
overlay.setAttribute("role", "dialog");
|
||||
overlay.setAttribute("aria-modal", "true");
|
||||
overlay.setAttribute("aria-label", "Vista ampliada de captura");
|
||||
|
||||
const img = document.createElement("img");
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.className = "lightbox-close";
|
||||
closeBtn.textContent = "\u00D7";
|
||||
closeBtn.setAttribute("aria-label", "Cerrar");
|
||||
|
||||
overlay.appendChild(img);
|
||||
overlay.appendChild(closeBtn);
|
||||
|
||||
function open(src, alt) {
|
||||
img.src = src;
|
||||
img.alt = alt;
|
||||
document.body.appendChild(overlay);
|
||||
document.body.style.overflow = "hidden";
|
||||
closeBtn.focus();
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (overlay.parentNode) {
|
||||
document.body.removeChild(overlay);
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
}
|
||||
|
||||
screenshots.forEach(function (el) {
|
||||
el.addEventListener("click", function () {
|
||||
open(el.src, el.alt);
|
||||
});
|
||||
});
|
||||
|
||||
overlay.addEventListener("click", function (e) {
|
||||
if (e.target === overlay) close();
|
||||
});
|
||||
|
||||
closeBtn.addEventListener("click", close);
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape") close();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
initLightbox();
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
|
||||
<rect width="200" height="200" fill="#0f3460" rx="12"/>
|
||||
<text x="100" y="90" text-anchor="middle" font-family="monospace" font-size="56" fill="#e94560">🚀</text>
|
||||
<text x="100" y="130" text-anchor="middle" font-family="monospace" font-size="13" fill="#eaeaea">COSMIC ESCAPE</text>
|
||||
<text x="100" y="155" text-anchor="middle" font-family="monospace" font-size="11" fill="#4ecca3">v1.2.0</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 496 B |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="450" viewBox="0 0 800 450">
|
||||
<rect width="800" height="450" fill="#0a0a1a"/>
|
||||
<rect x="0" y="380" width="800" height="70" fill="#1a1a2e"/>
|
||||
<circle cx="120" cy="80" r="40" fill="#f4d03f" opacity="0.8"/>
|
||||
<circle cx="300" cy="50" r="8" fill="#eaeaea" opacity="0.4"/>
|
||||
<circle cx="500" cy="120" r="5" fill="#eaeaea" opacity="0.5"/>
|
||||
<circle cx="700" cy="30" r="10" fill="#eaeaea" opacity="0.3"/>
|
||||
<rect x="350" y="250" width="40" height="60" fill="#4ecca3"/>
|
||||
<rect x="360" y="220" width="20" height="30" fill="#a0a0a0"/>
|
||||
<text x="400" y="430" text-anchor="middle" font-family="monospace" font-size="18" fill="#e94560">COSMIC ESCAPE — Nivel 1</text>
|
||||
<text x="400" y="230" text-anchor="middle" font-family="monospace" font-size="12" fill="#a0a0a0">[Captura 1 — Placeholder]</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 861 B |
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="450" viewBox="0 0 800 450">
|
||||
<rect width="800" height="450" fill="#16213e"/>
|
||||
<rect x="50" y="200" width="700" height="20" fill="#0f3460"/>
|
||||
<rect x="50" y="300" width="300" height="20" fill="#0f3460"/>
|
||||
<rect x="500" y="320" width="250" height="20" fill="#0f3460"/>
|
||||
<circle cx="200" cy="190" r="15" fill="#e94560" opacity="0.9"/>
|
||||
<circle cx="400" cy="290" r="15" fill="#e94560" opacity="0.7"/>
|
||||
<rect x="370" y="160" width="30" height="40" fill="#4ecca3"/>
|
||||
<text x="400" y="430" text-anchor="middle" font-family="monospace" font-size="18" fill="#e94560">COSMIC ESCAPE — Nivel 4</text>
|
||||
<text x="400" y="60" text-anchor="middle" font-family="monospace" font-size="12" fill="#a0a0a0">[Captura 2 — Placeholder]</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 799 B |
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="450" viewBox="0 0 800 450">
|
||||
<rect width="800" height="450" fill="#1a0a0a"/>
|
||||
<rect x="0" y="350" width="800" height="100" fill="#2a1a1a"/>
|
||||
<ellipse cx="400" cy="200" rx="200" ry="120" fill="#e94560" opacity="0.15"/>
|
||||
<circle cx="400" cy="200" r="60" fill="#c73650" opacity="0.6"/>
|
||||
<text x="400" y="208" text-anchor="middle" font-family="monospace" font-size="36" fill="#eaeaea">👾</text>
|
||||
<rect x="360" y="330" width="30" height="20" fill="#4ecca3"/>
|
||||
<text x="400" y="430" text-anchor="middle" font-family="monospace" font-size="18" fill="#e94560">COSMIC ESCAPE — Jefe Final</text>
|
||||
<text x="400" y="50" text-anchor="middle" font-family="monospace" font-size="12" fill="#a0a0a0">[Captura 3 — Placeholder]</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 797 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
|
||||
<rect width="200" height="200" fill="#16213e" rx="12"/>
|
||||
<rect x="60" y="60" width="20" height="20" fill="#4ecca3"/>
|
||||
<rect x="80" y="60" width="20" height="20" fill="#4ecca3"/>
|
||||
<rect x="100" y="60" width="20" height="20" fill="#4ecca3"/>
|
||||
<rect x="60" y="80" width="20" height="20" fill="#4ecca3"/>
|
||||
<rect x="100" y="80" width="20" height="20" fill="#4ecca3"/>
|
||||
<rect x="60" y="100" width="20" height="20" fill="#4ecca3"/>
|
||||
<rect x="80" y="100" width="20" height="20" fill="#4ecca3"/>
|
||||
<rect x="100" y="100" width="20" height="20" fill="#4ecca3"/>
|
||||
<text x="100" y="150" text-anchor="middle" font-family="monospace" font-size="11" fill="#eaeaea">PIXEL DUNGEON</text>
|
||||
<text x="100" y="170" text-anchor="middle" font-family="monospace" font-size="10" fill="#a0a0a0">v0.9.1</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 888 B |
@@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="450" viewBox="0 0 800 450">
|
||||
<rect width="800" height="450" fill="#0a0a12"/>
|
||||
<!-- Cuadrícula de tiles -->
|
||||
<rect x="100" y="100" width="600" height="260" fill="#16213e"/>
|
||||
<!-- Paredes -->
|
||||
<rect x="100" y="100" width="600" height="20" fill="#0f3460"/>
|
||||
<rect x="100" y="340" width="600" height="20" fill="#0f3460"/>
|
||||
<rect x="100" y="100" width="20" height="260" fill="#0f3460"/>
|
||||
<rect x="680" y="100" width="20" height="260" fill="#0f3460"/>
|
||||
<!-- Pasillos -->
|
||||
<rect x="300" y="100" width="20" height="80" fill="#0a0a12"/>
|
||||
<rect x="480" y="280" width="20" height="80" fill="#0a0a12"/>
|
||||
<!-- Jugador -->
|
||||
<rect x="190" y="200" width="16" height="16" fill="#4ecca3"/>
|
||||
<rect x="194" y="196" width="8" height="8" fill="#eaeaea"/>
|
||||
<!-- Enemigo -->
|
||||
<rect x="500" y="230" width="16" height="16" fill="#e94560"/>
|
||||
<rect x="504" y="226" width="8" height="8" fill="#a02040"/>
|
||||
<!-- Objetos -->
|
||||
<rect x="380" y="230" width="10" height="10" fill="#f4d03f"/>
|
||||
<rect x="420" y="210" width="8" height="14" fill="#8e44ad"/>
|
||||
<text x="400" y="430" text-anchor="middle" font-family="monospace" font-size="14" fill="#4ecca3">PIXEL DUNGEON — Nivel B1</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="80" viewBox="0 0 300 80">
|
||||
<rect width="300" height="80" fill="#1a1a2e" rx="6"/>
|
||||
<text x="20" y="52" font-family="monospace" font-size="36" font-weight="bold" fill="#e94560">JAIL</text>
|
||||
<text x="115" y="52" font-family="monospace" font-size="36" font-weight="bold" fill="#4ecca3">GAMES</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 365 B |