From 1ffca489e4ecafd0112215d30bb2c7499d49a954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Vidal?= Date: Thu, 19 Feb 2026 04:13:21 +0100 Subject: [PATCH] =?UTF-8?q?Primera=20versi=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 22 +++++++ Dockerfile | 22 +++++++ control.sh | 93 +++++++++++++++++++++++++++ docker-compose.yml | 12 ++++ next.config.js | 7 +++ package.json | 22 +++++++ src/app/Gallery.tsx | 150 ++++++++++++++++++++++++++++++++++++++++++++ src/app/layout.tsx | 18 ++++++ src/app/page.tsx | 40 ++++++++++++ tsconfig.json | 27 ++++++++ 10 files changed, 413 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 control.sh create mode 100644 docker-compose.yml create mode 100644 next.config.js create mode 100644 package.json create mode 100644 src/app/Gallery.tsx create mode 100644 src/app/layout.tsx create mode 100644 src/app/page.tsx create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..235808a --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Dependencias +node_modules/ +.pnp +.pnp.js + +# Compilación de Next.js +.next/ +out/ + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# OS +.DS_Store +Thumbs.db + +# Fotos (Opcional) +# Si no quieres subir tus fotos reales al repo de Gitea, +# descomenta la línea de abajo: +# public/imagenes/*.webp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..54d5c7b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM node:20-alpine + +# Establecemos el directorio de trabajo dentro del contenedor +WORKDIR /app + +# Copiamos los archivos de dependencias +COPY package*.json ./ + +# Instalamos las dependencias +RUN npm install + +# Copiamos el resto del proyecto +COPY . . + +# Construimos la aplicación Next.js +RUN npm run build + +# Exponemos el puerto +EXPOSE 3000 + +# Iniciamos el servidor en modo producción +CMD ["npm", "start"] diff --git a/control.sh b/control.sh new file mode 100644 index 0000000..49856cb --- /dev/null +++ b/control.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +# Colores para los mensajes +GREEN='\033[0;32m' +BLUE='\033[0;34m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Función de ayuda +show_help() { + echo -e "${BLUE}===================== CONTROL DEL BLOG DE JOSEMI =================${NC}" + echo " " + echo " Uso: ./control.sh [comando] " + echo "" + echo "Comandos disponibles:" + echo -e " ${GREEN}start${NC} -> Levanta todo el entorno (en segundo plano)" + echo -e " ${GREEN}stop${NC} -> Detiene los contenedores (sin borrarlos)" + echo -e " ${GREEN}restart${NC} -> Reinicia los contenedores" + echo -e " ${GREEN}build${NC} -> Reconstruye las imágenes (útil si instalas nuevas librerías)" + echo -e " ${GREEN}logs${NC} -> Ver logs de todo en tiempo real (Ctrl+C para salir)" + echo -e " ${GREEN}status${NC} -> Ver estado de los contenedores" + echo -e " ${GREEN}shell-be${NC} -> Entrar a la terminal del Backend" + echo -e " ${GREEN}shell-fe${NC} -> Entrar a la terminal del Frontend" + echo -e " ${RED}reset${NC} -> ¡PELIGRO! Borra la Base de Datos y la crea de cero (útil si cambias contraseñas)" + echo -e " ${YELLOW}info${NC} -> Muestra las URLs y credenciales de acceso" + echo -e " ${RED}down${NC} -> Deletea todo el container" + echo "==================================================================" +} + +# Lógica del script +case "$1" in + start) + echo -e "${GREEN}Arrancando el sistema...${NC}" + sudo docker-compose up -d + echo -e "${BLUE}¡Listo! Usa './control.sh info' para ver los accesos.${NC}" + ;; + stop) + echo -e "${YELLOW}Deteniendo contenedores...${NC}" + sudo docker-compose stop + ;; + restart) + echo -e "${YELLOW}Reiniciando...${NC}" + sudo docker-compose restart + ;; + build) + echo -e "${BLUE}Reconstruyendo imágenes (esto puede tardar)...${NC}" + sudo docker-compose up -d --build + ;; + logs) + echo -e "${BLUE}Mostrando logs (Ctrl+C para salir)...${NC}" + sudo docker-compose logs -f + ;; + status) + sudo docker-compose ps + ;; + shell-be) + echo -e "${GREEN}Entrando al contenedor del Backend...${NC}" + sudo docker exec -it blog_backend sh + ;; + shell-fe) + echo -e "${GREEN}Entrando al contenedor del Frontend...${NC}" + sudo docker exec -it blog_frontend sh + ;; + reset) + echo -e "${RED}ATENCIÓN: Esto borrará todos los datos de la base de datos.${NC}" + read -p "¿Estás seguro? (s/n): " confirm + if [[ $confirm == [sS] || $confirm == [sS][yY] ]]; then + echo -e "${YELLOW}Borrando todo...${NC}" + sudo docker-compose down -v + docker system prune -f + echo -e "${GREEN}Levantando de nuevo...${NC}" + sudo docker-compose up -d + else + echo "Operación cancelada." + fi + ;; + info) + echo -e "${BLUE}=== INFORMACIÓN DE ACCESO ===${NC}" + echo -e "Frontend (Web): ${GREEN}http://localhost:9001${NC}" + echo -e "Backend (API): ${GREEN}http://localhost:9002${NC}" + echo -e "Base de Datos: ${GREEN}localhost:9003${NC}" + echo -e "Usuario DB: ${YELLOW}josemi${NC}" + echo -e "Contraseña DB: ${YELLOW}josemivi${NC}" + ;; + down) + sudo docker-compose down -v + sudo docker system prune -f + ;; + *) + show_help + ;; +esac diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1926144 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +services: + portal-ferroviario: + build: . + container_name: portal_fotos_provincia + ports: + - "3000:3000" + volumes: + # Vinculamos la carpeta de fotos local con la del contenedor + - ./public/imagenes:/app/public/imagenes + environment: + - NODE_ENV=production + restart: unless-stopped diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..ae88795 --- /dev/null +++ b/next.config.js @@ -0,0 +1,7 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + swcMinify: true, +} + +module.exports = nextConfig diff --git a/package.json b/package.json new file mode 100644 index 0000000..eb080a6 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "album-fotos-provincia", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "next": "14.2.3", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "typescript": "^5" + } +} diff --git a/src/app/Gallery.tsx b/src/app/Gallery.tsx new file mode 100644 index 0000000..0ed6670 --- /dev/null +++ b/src/app/Gallery.tsx @@ -0,0 +1,150 @@ +'use client'; + +import React, { useState, useMemo } from 'react'; + +type ImageObj = { + fileName: string; + displayTitle: string; + tags: string[]; +}; + +export default function Gallery({ initialImages }: { initialImages: ImageObj[] }) { + const [searchTerm, setSearchTerm] = useState(''); + const [selectedImage, setSelectedImage] = useState(null); + + const filteredImages = useMemo(() => { + return initialImages.filter(img => { + const search = searchTerm.toLowerCase(); + return img.displayTitle.toLowerCase().includes(search) || + img.tags.some(t => t.includes(search)); + }); + }, [initialImages, searchTerm]); + + return ( +
+ {/* Header Estilizado */} +
+

Portal Ferroviario

+

Explora el patrimonio ferroviario de la provincia

+
+ +
+ {/* Buscador */} +
+ setSearchTerm(e.target.value)} + style={{ + width: '100%', + padding: '20px 30px', + fontSize: '1.2rem', + borderRadius: '50px', + border: 'none', + boxShadow: '0 10px 25px rgba(0,0,0,0.1)', + outline: 'none', + boxSizing: 'border-box' + }} + /> +
+ + {/* Grid de Fotos */} +
+ {filteredImages.map(img => ( +
setSelectedImage(img.fileName)} + style={{ + backgroundColor: 'white', + borderRadius: '16px', + overflow: 'hidden', + boxShadow: '0 4px 6px rgba(0,0,0,0.02)', + cursor: 'pointer', + transition: 'transform 0.3s ease', + display: 'flex', + flexDirection: 'column' + }} + onMouseEnter={(e) => e.currentTarget.style.transform = 'translateY(-5px)'} + onMouseLeave={(e) => e.currentTarget.style.transform = 'translateY(0)'} + > +
+ {img.displayTitle} +
+
+

+ {img.displayTitle} +

+
+ {img.tags.map((tag, idx) => ( + + {tag} + + ))} +
+
+
+ ))} +
+ + {/* Mensaje vacío */} + {filteredImages.length === 0 && ( +
+

No hay fotos que coincidan con tu búsqueda.

+
+ )} +
+ + {/* MODAL - Visor de imagen grande */} + {selectedImage && ( +
setSelectedImage(null)} + style={{ + position: 'fixed', + top: 0, left: 0, width: '100%', height: '100%', + backgroundColor: 'rgba(0,0,0,0.9)', + display: 'flex', alignItems: 'center', justifyContent: 'center', + zIndex: 1000, cursor: 'zoom-out', padding: '20px' + }} + > + Grande + +
+ )} +
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..6b54147 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,18 @@ +export const metadata = { + title: 'Portal Ferroviario', + description: 'Álbum de fotos ferroviarias de la provincia', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + {children} + + + ) +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..9baa8c3 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,40 @@ +import fs from 'fs'; +import path from 'path'; +import Gallery from './Gallery'; + +export default async function Page() { + const imagesDir = path.join(process.cwd(), 'public', 'imagenes'); + let images: { fileName: string, displayTitle: string, tags: string[] }[] = []; + + try { + if (fs.existsSync(imagesDir)) { + const fileNames = fs.readdirSync(imagesDir); + + images = fileNames + .filter(file => file.toLowerCase().endsWith('.webp')) + .map(fileName => { + // 1. Quitar la extensión .webp + const nameWithoutExt = fileName.replace(/\.webp$/i, ''); + + // 2. Título: Reemplazar guiones por espacios y poner bonita la primera letra + const displayTitle = nameWithoutExt.replace(/[-_]+/g, ' '); + + // 3. Tags: Separar por cualquier símbolo y tratar cada palabra como un tag individual + const tags = nameWithoutExt + .split(/[-_]+/) + .filter(word => word.length > 2) // Filtramos palabras muy cortas si quieres + .map(word => word.toLowerCase()); + + return { fileName, displayTitle, tags }; + }); + } + } catch (error) { + console.error("Error leyendo imágenes:", error); + } + + return ( +
+ +
+ ); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e59724b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}