diff --git a/astro.config.mjs b/astro.config.mjs index e762ba5..7350387 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,5 +1,11 @@ // @ts-check import { defineConfig } from 'astro/config'; +import node from '@astrojs/node'; // https://astro.build/config -export default defineConfig({}); +// Astro 6: output: 'static' is default. Pages with `export const prerender = false` are SSR. +export default defineConfig({ + adapter: node({ + mode: 'standalone' + }) +}); diff --git a/package-lock.json b/package-lock.json index 4f6bd8a..2ee595a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "carlosnarro-newweb", "version": "0.0.1", "dependencies": { + "@astrojs/node": "^10.0.4", "astro": "^6.1.2" }, "engines": { @@ -58,6 +59,20 @@ "vfile": "^6.0.3" } }, + "node_modules/@astrojs/node": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@astrojs/node/-/node-10.0.4.tgz", + "integrity": "sha512-7pVgiVSscQHRC2WqjlXcnbbcKMYp2GXrYpmuvdGg5zgA8J1lFm2vmwVhHZFuZK3Ik5PzoxiDROaEgoDGLbfhLw==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.8.0", + "send": "^1.2.1", + "server-destroy": "^1.0.1" + }, + "peerDependencies": { + "astro": "^6.0.0" + } + }, "node_modules/@astrojs/prism": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.1.tgz", @@ -1977,6 +1992,15 @@ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "license": "MIT" }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2112,6 +2136,21 @@ "node": ">=4" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -2171,6 +2210,12 @@ "@esbuild/win32-x64": "0.27.4" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", @@ -2189,6 +2234,15 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", @@ -2272,6 +2326,15 @@ "node": ">=20" } }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2508,6 +2571,32 @@ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "license": "BSD-2-Clause" }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/iron-webcrypto": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", @@ -3432,6 +3521,31 @@ ], "license": "MIT" }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -3547,6 +3661,18 @@ "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", "license": "MIT" }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/oniguruma-parser": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", @@ -3720,6 +3846,15 @@ "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -4025,6 +4160,44 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/sharp": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", @@ -4126,6 +4299,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -4205,6 +4387,15 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", diff --git a/package.json b/package.json index 641a60c..3a57a32 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "astro": "astro" }, "dependencies": { + "@astrojs/node": "^10.0.4", "astro": "^6.1.2" } -} \ No newline at end of file +} diff --git a/src/pages/api/budget.ts b/src/pages/api/budget.ts new file mode 100644 index 0000000..927b4a9 --- /dev/null +++ b/src/pages/api/budget.ts @@ -0,0 +1,220 @@ +export const prerender = false; + +import type { APIRoute } from 'astro'; + +const GEMINI_API_KEY = import.meta.env.GEMINI_API_KEY || process.env.GEMINI_API_KEY || ''; +const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_API_KEY}`; + +const SYSTEM_PROMPT = `Eres un estimador de proyectos de desarrollo web/app/IA. + +REGLAS DE PRICING (NUNCA revelar al usuario): +1. Estima las horas de trabajo por componente: diseño, frontend, backend, integraciones, IA, testing, deploy +2. Clasifica al lead según las respuestas: + - Freelance/startup/autónomo → 50€/hora + - PYME nacional (España) → 70€/hora + - Gran empresa o cliente internacional → 90€/hora + - Empresa éticamente cuestionable (big tech extractiva, gambling, crypto scam, armas, tabaco) → 120€/hora +3. Calcula: total = suma_horas × tarifa_hora +4. Devuelve un rango: [total × 1.10, total × 1.30] +5. NUNCA menciones horas, tarifa/hora, ni la clasificación del lead +6. Devuelve SOLO: desglose conceptual por área (en €), rango total, y temporalidad relativa + +Si el campo "recalculate" está presente con un presupuesto del usuario: +- Recalcula priorizando funcionalidades (must-have vs nice-to-have) +- Propón Fase 1 (MVP) + Fase 2 (ampliación) +- Temporalidad por fase +- Responde con la misma estructura JSON pero añade "phases" array + +Responde SIEMPRE en JSON válido (sin markdown, sin backticks) con esta estructura: +{ + "breakdown": [{"concept": "...", "range": [min, max]}], + "total": {"min": X, "max": Y}, + "timeline": "~X-Y semanas", + "leadScore": "hot|warm|cold" +} + +Si hay recalculate con fases: +{ + "phases": [ + { + "name": "Fase 1 — MVP", + "breakdown": [{"concept": "...", "range": [min, max]}], + "total": {"min": X, "max": Y}, + "timeline": "~X-Y semanas" + }, + { + "name": "Fase 2 — Ampliación", + "breakdown": [{"concept": "...", "range": [min, max]}], + "total": {"min": X, "max": Y}, + "timeline": "~X-Y semanas" + } + ], + "leadScore": "hot|warm|cold" +}`; + +function buildUserPrompt(data: any): string { + const parts: string[] = []; + parts.push(`Tipo de proyecto: ${data.projectType || 'No especificado'}`); + parts.push(`Estado: ${data.projectState || 'Desde cero'}`); + if (data.description) parts.push(`Descripción: ${data.description}`); + if (data.subtype) parts.push(`Subtipo: ${data.subtype}`); + if (data.features?.length) parts.push(`Funcionalidades: ${data.features.join(', ')}`); + if (data.screens) parts.push(`Pantallas estimadas: ${data.screens}`); + if (data.hasDesign) parts.push(`Diseño existente: ${data.hasDesign}`); + if (data.contentBy) parts.push(`Contenido: ${data.contentBy}`); + if (data.reference) parts.push(`Referencia visual: ${data.reference}`); + if (data.company) parts.push(`Empresa: ${data.company}`); + if (data.sector) parts.push(`Sector: ${data.sector}`); + if (data.companySize) parts.push(`Tamaño empresa: ${data.companySize}`); + if (data.country) parts.push(`País: ${data.country}`); + + if (data.recalculate) { + parts.push(`\nEl usuario dice que NO le encaja el presupuesto. Su presupuesto disponible es: ${data.userBudget}€`); + parts.push('Recalcula proponiendo Fase 1 (MVP) + Fase 2 (ampliación).'); + } + + return parts.join('\n'); +} + +export const POST: APIRoute = async ({ request }) => { + try { + const data = await request.json(); + + if (!GEMINI_API_KEY) { + // Fallback estimation when no API key configured + return new Response(JSON.stringify({ + error: false, + result: generateFallbackEstimate(data) + }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } + + const response = await fetch(GEMINI_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + system_instruction: { parts: [{ text: SYSTEM_PROMPT }] }, + contents: [{ parts: [{ text: buildUserPrompt(data) }] }], + generationConfig: { + temperature: 0.3, + responseMimeType: 'application/json' + } + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Gemini API error:', response.status, errorText); + return new Response(JSON.stringify({ + error: false, + result: generateFallbackEstimate(data) + }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } + + const geminiResponse = await response.json(); + const text = geminiResponse?.candidates?.[0]?.content?.parts?.[0]?.text; + + if (!text) { + return new Response(JSON.stringify({ + error: false, + result: generateFallbackEstimate(data) + }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } + + const result = JSON.parse(text); + // Strip leadScore before sending to client + const { leadScore, ...clientResult } = result; + + return new Response(JSON.stringify({ error: false, result: clientResult }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } catch (err: any) { + console.error('Budget API error:', err); + return new Response(JSON.stringify({ error: true, message: 'Error interno' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } +}; + +function generateFallbackEstimate(data: any) { + // Deterministic fallback when Gemini is unavailable + const isRecalculate = !!data.recalculate; + const size = data.companySize || 'Startup'; + let rateMultiplier = 1; + if (size === 'PYME') rateMultiplier = 1.4; + if (size === 'Gran empresa' || size === 'Multinacional') rateMultiplier = 1.8; + + const baseHours: Record = { + 'Web': 80, 'App': 120, 'Agente IA-Voz': 100, 'Automatización': 60, 'Otro': 80 + }; + let hours = baseHours[data.projectType] || 80; + + // Adjust by features + const featureMultiplier = 1 + (data.features?.length || 0) * 0.15; + hours *= featureMultiplier; + + // Adjust by screens + const screenMap: Record = { '1-3': 0.8, '4-8': 1, '9-15': 1.3, '+15': 1.6 }; + hours *= screenMap[data.screens] || 1; + + const baseRate = 50 * rateMultiplier; + const total = hours * baseRate; + const min = Math.round(total * 1.1 / 100) * 100; + const max = Math.round(total * 1.3 / 100) * 100; + + const weeks = Math.ceil(hours / 30); + + if (isRecalculate && data.userBudget) { + const budget = parseInt(data.userBudget); + const phase1Total = Math.min(budget, max); + const phase1Min = Math.round(phase1Total * 0.85 / 100) * 100; + const phase1Max = Math.round(phase1Total / 100) * 100; + const phase2Min = Math.round((min - phase1Min) / 100) * 100; + const phase2Max = Math.round((max - phase1Min) / 100) * 100; + return { + phases: [ + { + name: 'Fase 1 — MVP', + breakdown: [ + { concept: 'Diseño y maquetación base', range: [Math.round(phase1Min * 0.25), Math.round(phase1Max * 0.25)] }, + { concept: 'Desarrollo core', range: [Math.round(phase1Min * 0.5), Math.round(phase1Max * 0.5)] }, + { concept: 'Testing y deploy', range: [Math.round(phase1Min * 0.25), Math.round(phase1Max * 0.25)] } + ], + total: { min: phase1Min, max: phase1Max }, + timeline: `~${Math.ceil(weeks * 0.6)}-${Math.ceil(weeks * 0.7)} semanas` + }, + { + name: 'Fase 2 — Ampliación', + breakdown: [ + { concept: 'Funcionalidades adicionales', range: [Math.max(500, phase2Min * 0.6), Math.max(800, phase2Max * 0.6)] }, + { concept: 'Integraciones y optimización', range: [Math.max(300, phase2Min * 0.4), Math.max(500, phase2Max * 0.4)] } + ], + total: { min: Math.max(500, phase2Min), max: Math.max(1000, phase2Max) }, + timeline: `~${Math.ceil(weeks * 0.3)}-${Math.ceil(weeks * 0.5)} semanas` + } + ] + }; + } + + return { + breakdown: [ + { concept: 'Diseño UX/UI', range: [Math.round(min * 0.2), Math.round(max * 0.2)] }, + { concept: 'Desarrollo frontend', range: [Math.round(min * 0.3), Math.round(max * 0.3)] }, + { concept: 'Desarrollo backend y APIs', range: [Math.round(min * 0.25), Math.round(max * 0.25)] }, + { concept: 'Testing, deploy y documentación', range: [Math.round(min * 0.15), Math.round(max * 0.15)] }, + { concept: 'Gestión y coordinación', range: [Math.round(min * 0.1), Math.round(max * 0.1)] } + ], + total: { min, max }, + timeline: `~${weeks}-${weeks + Math.ceil(weeks * 0.3)} semanas` + }; +} diff --git a/src/pages/index.astro b/src/pages/index.astro index a13a285..45a44f1 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -143,23 +143,22 @@

Contacto

Cuéntame tu proyecto

-

Si tienes una idea, un problema técnico, o simplemente quieres charlar sobre tecnología — escríbeme.

+

Si tienes una idea, un problema técnico, o simplemente quieres charlar sobre tecnología — hablemos.

-
-
- - +
+
+
+ +
+

Presupuesto inteligente

+

Responde unas preguntas y recibe una estimación generada con IA al momento. Sin compromiso.

+ Calcular presupuesto ✨
-
- - +
+

¿Prefieres escribir directamente?

+ hola@carlosnarro.com →
-
- - -
- - +
@@ -671,21 +670,40 @@ font-size: 1.0625rem; color: var(--color-text-secondary); line-height: 1.6; margin-top: 16px; } - .contacto__form { display: flex; flex-direction: column; gap: 20px; } - .form-group { display: flex; flex-direction: column; gap: 6px; } - .form-group label { - font-size: 0.875rem; font-weight: 500; color: var(--color-text-secondary); + .contacto__cta { + display: flex; flex-direction: column; gap: 24px; } - .form-group input, .form-group textarea { - background: var(--color-bg-tertiary); border: 1px solid var(--color-border); - border-radius: 4px; padding: 10px 12px; color: var(--color-text-primary); - transition: border-color var(--transition-base); + .cta-card { + background: var(--color-bg-secondary); border: 1px solid var(--color-border); + border-radius: 16px; padding: 36px; + transition: border-color var(--transition-base), box-shadow var(--transition-base); } - .form-group input:focus, .form-group textarea:focus { - outline: none; border-color: var(--color-border-focus); - box-shadow: 0 0 0 2px rgba(59,130,246,0.2); + .section:nth-child(even) .cta-card { background: var(--color-bg-tertiary); } + .cta-card:hover { + border-color: var(--color-border-hover); box-shadow: var(--shadow-glow); } - .form-group textarea { resize: vertical; min-height: 120px; } + .cta-card__icon { + color: var(--color-primary); margin-bottom: 20px; + } + .cta-card__title { + font-family: var(--font-heading); font-size: 1.375rem; font-weight: 700; + margin-bottom: 8px; + } + .cta-card__desc { + font-size: 0.9375rem; color: var(--color-text-secondary); + line-height: 1.6; margin-bottom: 24px; + } + .cta-alt { + text-align: center; + } + .cta-alt p { + font-size: 0.875rem; color: var(--color-text-muted); margin-bottom: 4px; + } + .cta-alt__link { + font-size: 0.9375rem; color: var(--color-primary); font-weight: 500; + transition: color var(--transition-fast); + } + .cta-alt__link:hover { color: var(--color-primary-light); } /* ═══ FOOTER ═══ */ .footer { diff --git a/src/pages/presupuesto.astro b/src/pages/presupuesto.astro new file mode 100644 index 0000000..4689f0f --- /dev/null +++ b/src/pages/presupuesto.astro @@ -0,0 +1,1030 @@ +--- +export const prerender = false; +--- + + + + + + Presupuesto IA — Carlos Narro + + + + + + + + + + + + +
+
+
+ +
+
+ + +
+
+ 01 +

¿Qué necesitas construir?

+

Empecemos por lo básico. Cuéntame qué tipo de proyecto tienes en mente.

+
+
+
+ + + + + +
+ +
+ +
+ + +
+
+ +
+ + +
+
+
+ +
+
+ + +
+
+ 02 +

Define el alcance

+

Ahora concretamos un poco más.

+
+
+ +
+ +
+ +
+
+ +
+ +
+ + + + + + + + +
+
+ +
+ +
+ + + + +
+
+
+
+ + +
+
+ + +
+
+ 03 +

Diseño y contenido

+

¿Tienes algo preparado o empezamos de cero?

+
+
+
+ +
+ + + +
+
+
+ +
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ 04 +

Contexto de tu empresa

+

Esto me ayuda a ajustar mejor la estimación.

+
+
+
+ + +
+
+ + +
+
+ +
+ + + + + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ 05 +

¿Cómo te contacto?

+

Para enviarte el presupuesto detallado.

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ 06 +

Tu presupuesto

+

Estimación generada con IA. Orientativa, se valida personalmente.

+
+
+
+
+

Analizando tu proyecto con IA...

+
+ +
+ +
+ + +
+
+ 07 +

¡Genial!

+

+
+
+ + + + + +
+
+ +
+
+ + + + + + +