ADD new frontend

This commit is contained in:
Urtzi Alfaro
2025-08-28 10:41:04 +02:00
parent 9c247a5f99
commit 0fd273cfce
492 changed files with 114979 additions and 1632 deletions

View File

@@ -35,6 +35,7 @@ volumes:
model_storage:
log_storage:
nominatim_data:
frontend_node_modules:
# ================================================================
@@ -1063,7 +1064,7 @@ services:
ipv4_address: 172.20.0.110
volumes:
- ./frontend:/app
- /app/node_modules # Exclude node_modules from bind mount
- frontend_node_modules:/app/node_modules # Use named volume for node_modules
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
interval: 30s

View File

@@ -0,0 +1,38 @@
# frontend/Dockerfile.development - FIXED VERSION
FROM node:18-alpine
# Install curl for healthchecks
RUN apk add --no-cache curl
# Set working directory
WORKDIR /app
# Create non-root user for security but don't switch yet
RUN addgroup -g 1001 -S nodejs && \
adduser -S reactjs -u 1001 -G nodejs
# Copy package files first (better caching)
COPY package*.json ./
# Install all dependencies (including dev dependencies) as root
RUN npm ci --verbose && \
npm cache clean --force
# Copy source code
COPY . .
# Change ownership of all files to the non-root user
RUN chown -R reactjs:nodejs /app
# Now switch to non-root user
USER reactjs
# Expose port 3000 (Vite default)
EXPOSE 3000
# Add healthcheck
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD curl -f http://localhost:3000/ || exit 1
# Start development server with host binding
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

View File

@@ -0,0 +1,41 @@
# frontend/Dockerfile.production
# Multi-stage build for production
# Build stage
FROM node:18-alpine as builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Production stage
FROM nginx:alpine
# Install curl for healthchecks
RUN apk add --no-cache curl
# Copy built app from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Expose port 80
EXPOSE 80
# Add healthcheck
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost/ || exit 1
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

170
fdev-ffrontend/index.html Normal file
View File

@@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Primary Meta Tags -->
<title>PanIA - Inteligencia Artificial para tu Panadería en Madrid</title>
<meta name="title" content="PanIA - Inteligencia Artificial para tu Panadería en Madrid" />
<meta name="description" content="La primera IA diseñada para panaderías españolas. Reduce desperdicios hasta un 25%, aumenta ganancias y optimiza producción con predicciones precisas en Madrid." />
<meta name="keywords" content="inteligencia artificial panadería, predicción ventas panadería, IA para panaderías Madrid, sistema predicción panadería, optimización panadería, reducir desperdicios panadería" />
<meta name="author" content="PanIA Team" />
<meta name="robots" content="index, follow" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://pania.es/" />
<meta property="og:title" content="PanIA - Inteligencia Artificial para tu Panadería en Madrid" />
<meta property="og:description" content="La primera IA diseñada para panaderías españolas. Reduce desperdicios hasta un 25% y optimiza tu producción con predicciones precisas." />
<meta property="og:image" content="https://pania.es/og-image.jpg" />
<meta property="og:locale" content="es_ES" />
<meta property="og:site_name" content="PanIA" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://pania.es/" />
<meta property="twitter:title" content="PanIA - Inteligencia Artificial para tu Panadería en Madrid" />
<meta property="twitter:description" content="La primera IA diseñada para panaderías españolas. Reduce desperdicios hasta un 25% y optimiza tu producción con predicciones precisas." />
<meta property="twitter:image" content="https://pania.es/twitter-image.jpg" />
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<!-- Preconnect to important domains -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet" />
<!-- Local Business Schema.org markup -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "PanIA",
"description": "Inteligencia Artificial para panaderías en Madrid",
"url": "https://pania.es",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Web",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "EUR",
"description": "30 días de prueba gratuita"
},
"provider": {
"@type": "Organization",
"name": "PanIA",
"url": "https://pania.es",
"address": {
"@type": "PostalAddress",
"addressLocality": "Madrid",
"@country": "España"
}
},
"targetAudience": {
"@type": "Audience",
"name": "Panaderías en Madrid"
},
"featureList": [
"Predicciones de demanda con IA",
"Reducción de desperdicios",
"Optimización de producción",
"Gestión inteligente de pedidos"
]
}
</script>
<!-- Performance and Analytics -->
<link rel="dns-prefetch" href="//api.pania.es" />
<!-- Prevent FOUC (Flash of Unstyled Content) -->
<style>
/* Critical CSS for initial paint */
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
margin: 0;
padding: 0;
background-color: #fafafa;
}
/* Loading spinner for initial load */
.initial-loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #fff7ed 0%, #fed7aa 100%);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-content {
text-align: center;
}
.bakery-icon {
width: 80px;
height: 80px;
background: #f97316;
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
font-size: 32px;
animation: pulse 2s ease-in-out infinite;
}
.loading-text {
color: #9a3412;
font-size: 18px;
font-weight: 500;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
</style>
</head>
<body>
<!-- Initial loading screen -->
<div id="initial-loading" class="initial-loading">
<div class="loading-content">
<div class="bakery-icon">🥖</div>
<div class="loading-text">Cargando PanIA...</div>
</div>
</div>
<!-- React app container -->
<div id="root"></div>
<!-- Hide loading screen once React loads -->
<script>
// Hide loading screen when React app is ready
window.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
const loadingScreen = document.getElementById('initial-loading');
if (loadingScreen) {
loadingScreen.style.opacity = '0';
loadingScreen.style.transition = 'opacity 0.5s ease-out';
setTimeout(() => {
loadingScreen.style.display = 'none';
}, 500);
}
}, 1000); // Show loading for at least 1 second
});
</script>
<!-- Main application script -->
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

70
fdev-ffrontend/nginx.conf Normal file
View File

@@ -0,0 +1,70 @@
# frontend/nginx.conf
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Enable gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Handle React Router
location / {
try_files $uri $uri/ /index.html;
}
# API proxy to backend
location /api/ {
proxy_pass http://bakery-gateway:8000/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}

7652
fdev-ffrontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
{
"name": "pania-frontend",
"version": "1.0.0",
"description": "AI-powered bakery demand forecasting platform for Madrid",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:fix": "eslint . --ext ts,tsx --fix"
},
"dependencies": {
"@hookform/resolvers": "^3.3.1",
"@reduxjs/toolkit": "^1.9.5",
"@stripe/react-stripe-js": "^3.9.0",
"@stripe/stripe-js": "^7.8.0",
"clsx": "^2.0.0",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"i18next": "^23.4.4",
"i18next-browser-languagedetector": "^7.1.0",
"lucide-react": "^0.263.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.45.4",
"react-hot-toast": "^2.4.1",
"react-i18next": "^13.1.2",
"react-redux": "^8.1.2",
"react-router-dom": "^6.15.0",
"recharts": "^2.8.0",
"tailwind-merge": "^1.14.0",
"zod": "^3.22.2"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.4",
"@tailwindcss/typography": "^0.5.9",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
"@vitest/ui": "^0.34.1",
"autoprefixer": "^10.4.14",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.27",
"tailwindcss": "^3.3.0",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vitest": "^0.34.1"
},
"keywords": [
"bakery",
"forecasting",
"ai",
"madrid",
"react",
"typescript",
"tailwind"
],
"repository": {
"type": "git",
"url": "https://github.com/pania-es/frontend"
},
"author": "PanIA Team",
"license": "MIT"
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1,67 @@
import React, { useEffect } from 'react';
import { RouterProvider } from 'react-router-dom';
import { Provider } from 'react-redux';
import { Toaster } from 'react-hot-toast';
import { router } from './router';
import { store } from './store';
import ErrorBoundary from './components/ErrorBoundary';
import { useAuth } from './hooks/useAuth';
// i18n
import './i18n';
// Global styles
import './styles/globals.css';
const AppContent: React.FC = () => {
const { initializeAuth } = useAuth();
useEffect(() => {
initializeAuth();
}, [initializeAuth]);
return (
<ErrorBoundary>
<div className="App min-h-screen bg-gray-50">
<RouterProvider router={router} />
{/* Global Toast Notifications */}
<Toaster
position="top-right"
toastOptions={{
duration: 4000,
style: {
background: '#fff',
color: '#333',
boxShadow: '0 4px 25px -5px rgba(0, 0, 0, 0.1)',
borderRadius: '12px',
padding: '16px',
},
success: {
iconTheme: {
primary: '#22c55e',
secondary: '#fff',
},
},
error: {
iconTheme: {
primary: '#ef4444',
secondary: '#fff',
},
},
}}
/>
</div>
</ErrorBoundary>
);
};
const App: React.FC = () => {
return (
<Provider store={store}>
<AppContent />
</Provider>
);
};
export default App;

Some files were not shown because too many files have changed in this diff Show More