ADD new frontend
This commit is contained in:
@@ -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
|
||||
|
||||
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