ADD new frontend
This commit is contained in:
@@ -35,6 +35,7 @@ volumes:
|
|||||||
model_storage:
|
model_storage:
|
||||||
log_storage:
|
log_storage:
|
||||||
nominatim_data:
|
nominatim_data:
|
||||||
|
frontend_node_modules:
|
||||||
|
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
@@ -1063,7 +1064,7 @@ services:
|
|||||||
ipv4_address: 172.20.0.110
|
ipv4_address: 172.20.0.110
|
||||||
volumes:
|
volumes:
|
||||||
- ./frontend:/app
|
- ./frontend:/app
|
||||||
- /app/node_modules # Exclude node_modules from bind mount
|
- frontend_node_modules:/app/node_modules # Use named volume for node_modules
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
|
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|||||||
38
fdev-ffrontend/Dockerfile.development
Normal file
38
fdev-ffrontend/Dockerfile.development
Normal 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"]
|
||||||
41
fdev-ffrontend/Dockerfile.production
Normal file
41
fdev-ffrontend/Dockerfile.production
Normal 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
170
fdev-ffrontend/index.html
Normal 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
70
fdev-ffrontend/nginx.conf
Normal 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
7652
fdev-ffrontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
75
fdev-ffrontend/package.json
Normal file
75
fdev-ffrontend/package.json
Normal 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"
|
||||||
|
}
|
||||||
6
fdev-ffrontend/postcss.config.js
Normal file
6
fdev-ffrontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
67
fdev-ffrontend/src/App.tsx
Normal file
67
fdev-ffrontend/src/App.tsx
Normal 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
Reference in New Issue
Block a user