diff --git a/.agents/skills/conductor-implement/SKILL.md b/.agents/skills/conductor-implement/SKILL.md new file mode 100644 index 0000000..8b970be --- /dev/null +++ b/.agents/skills/conductor-implement/SKILL.md @@ -0,0 +1,19 @@ +# Conductor Implement + +Implement an approved track one task at a time. + +## When to Use +- After a track has been created with `conductor-newTrack` and approved +- When ready to write code + +## Steps +1. Read the track's `tasks.md` from `conductor/tracks//` +2. Implement tasks one at a time, validating each +3. Do not make changes outside the spec without asking +4. Update task status as you progress +5. Run lint/typecheck after each significant change + +## Rules +- Stay within the spec boundaries +- If you discover something that contradicts the spec, stop and ask +- Save important decisions to Engram with `mem_save` diff --git a/.agents/skills/conductor-newTrack/SKILL.md b/.agents/skills/conductor-newTrack/SKILL.md new file mode 100644 index 0000000..880dd05 --- /dev/null +++ b/.agents/skills/conductor-newTrack/SKILL.md @@ -0,0 +1,23 @@ +# Conductor New Track + +Create a new feature/bugfix/refactor track with spec, plan, tasks, risks, and acceptance criteria. + +## When to Use +- Before implementing any new feature or significant change +- When requirements are ambiguous and need clarification +- For bug fixes that require architectural changes + +## Steps +1. Recover Engram memory for `computerselectorhelper` using `mem_context` then `mem_search` +2. Treat recovered memories as constraints +3. Create a track in `conductor/tracks/` with: + - `spec.md` — What needs to be built and why + - `plan.md` — How it will be built, step by step + - `tasks.md` — Granular implementation tasks + - `risks.md` — Potential issues and mitigations + - `acceptance.md` — Criteria to consider the track done +4. Present the track for review before implementation + +## Output +- New track directory under `conductor/tracks//` +- All spec documents filled out diff --git a/.agents/skills/conductor-revert/SKILL.md b/.agents/skills/conductor-revert/SKILL.md new file mode 100644 index 0000000..844aaa6 --- /dev/null +++ b/.agents/skills/conductor-revert/SKILL.md @@ -0,0 +1,14 @@ +# Conductor Revert + +Revert a track only when explicitly requested by the user. + +## When to Use +- ONLY when the user explicitly asks to undo/revert a track +- Never proactively + +## Steps +1. Identify the track to revert +2. Undo all changes made during implementation +3. Restore files to their pre-implementation state +4. Update track status to "reverted" +5. Document the revert reason in Engram diff --git a/.agents/skills/conductor-review/SKILL.md b/.agents/skills/conductor-review/SKILL.md new file mode 100644 index 0000000..a7d0dd9 --- /dev/null +++ b/.agents/skills/conductor-review/SKILL.md @@ -0,0 +1,16 @@ +# Conductor Review + +Review a track's implementation against its spec, acceptance criteria, Engram memory, and project rules. + +## When to Use +- Before closing a track as complete +- When the user asks to review work +- Before merging or deploying + +## Steps +1. Read the track's `spec.md`, `acceptance.md`, and `plan.md` +2. Compare the actual implementation against the spec +3. Check against Engram memory for `computerselectorhelper` for any constraints +4. Verify acceptance criteria are met +5. Report any gaps or issues +6. If all good, mark track as completed and save summary to Engram diff --git a/.agents/skills/conductor-setup/SKILL.md b/.agents/skills/conductor-setup/SKILL.md new file mode 100644 index 0000000..344bd1e --- /dev/null +++ b/.agents/skills/conductor-setup/SKILL.md @@ -0,0 +1,17 @@ +# Conductor Setup + +Initialize the SDD/Conductor project structure. + +## When to Use +- First time setting up SDD workflow in this project +- When `conductor/` directory doesn't exist + +## Steps +1. Check if `conductor/` directory exists +2. If not, create `conductor/tracks/` structure +3. Create `conductor/config.json` with project metadata +4. Document the setup in Engram memory + +## Output +- `conductor/config.json` +- `conductor/tracks/` directory ready for new tracks diff --git a/.agents/skills/conductor-status/SKILL.md b/.agents/skills/conductor-status/SKILL.md new file mode 100644 index 0000000..12c68f4 --- /dev/null +++ b/.agents/skills/conductor-status/SKILL.md @@ -0,0 +1,13 @@ +# Conductor Status + +Inspect current tracks and their progress. + +## When to Use +- To check what tracks are active +- To see progress on ongoing work +- Before starting new work to avoid conflicts + +## Steps +1. List all tracks in `conductor/tracks/` +2. Show status of each track (pending, in-progress, completed) +3. Show current task being worked on for active tracks diff --git a/.engram/config.json b/.engram/config.json new file mode 100644 index 0000000..de0cfe1 --- /dev/null +++ b/.engram/config.json @@ -0,0 +1,3 @@ +{ + "project_name": "computerselectorhelper" +} diff --git a/.opencode/agents/ui-ux-advisor.md b/.opencode/agents/ui-ux-advisor.md new file mode 100644 index 0000000..9ab7d72 --- /dev/null +++ b/.opencode/agents/ui-ux-advisor.md @@ -0,0 +1,91 @@ +--- +description: UI/UX advisor expert in React, SEO optimization, and user experience design. Reviews interfaces for accessibility, modern design patterns, and SEO best practices. +mode: subagent +model: openai/gpt-5.5 +temperature: 0.3 +permission: + edit: ask + bash: deny + read: allow + glob: allow + grep: allow + webfetch: allow +--- + +You are a senior UI/UX advisor specializing in React applications, SEO optimization, and user experience design. Your expertise includes: + +## Core Responsibilities + +1. **React UI/UX Review** + - Component structure and composition best practices + - Modern React patterns (hooks, context, memoization) + - Responsive design implementation + - Animation and transition quality + - State management for UI flows + +2. **SEO Optimization** + - Meta tags and structured data (JSON-LD, Open Graph, Twitter Cards) + - Semantic HTML and accessibility landmarks + - Performance impact on SEO (Core Web Vitals) + - Dynamic meta management (react-helmet-async patterns) + - Sitemap and robots.txt configuration + +3. **User Experience Design** + - Information architecture and user flows + - Progressive disclosure patterns + - Form design and validation UX + - Loading states and error handling UX + - Mobile-first responsive design + +4. **Accessibility (a11y)** + - WCAG 2.1 AA compliance + - ARIA attributes and roles + - Keyboard navigation and focus management + - Color contrast and visual accessibility + - Screen reader compatibility + +## Review Guidelines + +When reviewing UI/UX: + +- **Be specific**: Point to exact files, lines, and components +- **Provide examples**: Show before/after code snippets +- **Prioritize**: Critical issues first, then improvements +- **Consider context**: Account for the project's design system and constraints +- **Balance aesthetics and function**: Beautiful but unusable is as bad as usable but ugly + +## SEO Review Checklist + +- [ ] Title tags unique and descriptive per page +- [ ] Meta descriptions compelling and keyword-relevant +- [ ] Open Graph tags complete (title, description, image, url, type) +- [ ] Twitter Card tags present +- [ ] JSON-LD structured data appropriate for content type +- [ ] Semantic HTML landmarks (header, main, nav, footer) +- [ ] Images have descriptive alt text +- [ ] Internal linking structure logical +- [ ] Mobile-responsive design verified +- [ ] Page load performance optimized + +## UI/UX Review Checklist + +- [ ] Visual hierarchy clear and scannable +- [ ] Consistent spacing and typography +- [ ] Interactive elements have clear affordances +- [ ] Loading states for all async operations +- [ ] Error states with actionable messages +- [ ] Empty states with guidance +- [ ] Mobile breakpoints tested (320px, 375px, 768px, 1024px) +- [ ] Touch targets minimum 44x44px +- [ ] Focus indicators visible for keyboard users +- [ ] Color contrast meets WCAG AA (4.5:1 for text) + +## Communication Style + +- Professional but approachable +- Use concrete examples from the codebase +- Explain the "why" behind recommendations +- Acknowledge trade-offs and constraints +- Suggest incremental improvements when full redesign isn't feasible + +Focus on actionable improvements that enhance both user satisfaction and search visibility. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a2570f3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,79 @@ +# ComputerSelectorHelper Agent Guide + +## Memory First + +- Before planning or coding, recover Engram context for project `computerselectorhelper` with `mem_context` first, then `mem_search` when needed. +- Treat recovered memories and local docs as constraints. +- Save meaningful decisions, discoveries, bug fixes, and completed-track summaries back to Engram with `mem_save` or `mem_session_summary`. + +## Project Context + +ComputerSelectorHelper is a React + Vite web app that helps users choose the right laptop based on their needs, budget, and usage patterns. Users answer a quiz and receive personalized spec recommendations with suggested laptop models. + +### Stack +- React 19 with JSX +- Vite 6 (bundler) +- React Router DOM 7 +- React Icons +- Vercel Analytics +- ESLint 9 + +### Product Goals +- Help non-technical users make informed laptop purchase decisions +- Provide clear technical explanations in plain language +- Suggest real laptop models with approximate prices (MXN) + +### Non-Goals +- Not an e-commerce platform +- Not a real-time price tracker +- Not a professional review site + +### Important Local Docs +- `E:\Job\ENGRAM_LOCAL_COMANDOS.md` — Engram CLI commands +- `E:\Job\NUEVO_PROYECTO_SDD_ENGRAM.md` — New project setup guide +- `E:\Job\SDD_ENGRAM_WORKFLOW.md` — SDD workflow patterns +- `docs/SDD_SKILLS_USAGE.md` — SDD skills usage for this project + +## SDD / Skills Workflow + +- Use Conductor skills in `.agents/skills/` for spec-driven work. +- For new features or ambiguous changes, start with `conductor-setup` if the Conductor structure is missing, then use `conductor-newTrack` before implementation. +- Use `conductor-implement` only after the track/spec/tasks are clear. +- Use `conductor-status` to inspect active tracks, `conductor-review` before closing work, and `conductor-revert` only when the user explicitly asks to undo a track. +- Keep tiny fixes direct when a full SDD track would add unnecessary process. + +## Dream Team Architecture + +This project uses a multi-agent orchestration model: + +| Role | Agent | Model | Responsibility | +|------|-------|-------|----------------| +| Principal (Orchestrator/Auditor) | AgentPrincipal | qwen3.6-plus | Delegates, supervises, manages branches/tickets, reports to user | +| Architecture & Security | AgentArquitecto | deepseek-v4-pro | Architecture decisions, security audits, code quality | +| QA & Product | AgentQA | GLM-5 | Testing, edge cases, UX, acceptance criteria | +| Performance & Resilience | AgentPerformance | minimax-m2.7 | Performance optimization, memory, resilience, idempotency | +| Research & Traceability | AgentResearch | gemini-2.5-pro (Google provider) | Research, precedents, consistency with prior decisions | +| Build Worker | KimiBuildWorker | kimi-k2.6 | Builds, compilation, CI execution | +| Build Fixer | JuniorQwen | MiMo 2.5V Pro | Build fixes, regression diagnosis, hotfixes | +| UI/UX Advisor | UIUXAdvisor | openai/gpt-5.5 | React UI/UX review, SEO optimization, accessibility, modern design patterns | + +### Agent Workflow +1. **Plan**: Auditor presents quick questions, consults agents, agrees on requirements, edge cases +2. **Confirm**: User confirms requirements and expected cases +3. **Delegate**: Auditor assigns tickets to agents for documentation and implementation +4. **Implement**: Agents code according to specs +5. **Cross-Review**: All agents review each other's work, document issues +6. **Report**: Auditor presents results, waits for user feedback + +## Worktree Safety + +- Do not revert or overwrite existing changes unless explicitly requested. +- Do not commit unless the user explicitly asks. +- Do not expose or commit secrets, `.env`, tokens, or credentials. + +## Known Issues to Address + +- Missing SEO (meta tags, Open Graph, structured data, sitemap) +- Inference engine has bugs and needs more complex logic +- UI needs polish and refinement +- Project is ~1 year old and needs updates diff --git a/README.md b/README.md index 7059a96..8a0bc3b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,152 @@ -# React + Vite +# Computer Selector Helper 🖥️ -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +> Respondé unas preguntas y recibí una recomendación personalizada de laptop según tu presupuesto y necesidades reales. -Currently, two official plugins are available: +**[🌐 Ver en vivo](https://computerselectorhelper.vercel.app/)** -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +## ¿Qué es? -## Expanding the ESLint configuration +Computer Selector Helper es un asistente web que ayuda a usuarios no técnicos a elegir la laptop ideal. A través de un cuestionario interactivo de 10 preguntas, el motor de inferencia analiza tus necesidades de uso, presupuesto y preferencias para recomendarte: -If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. +- ✅ Especificaciones técnicas personalizadas (CPU, GPU, RAM, almacenamiento) +- ✅ Modelos de laptops reales con precios aproximados en MXN +- ✅ Advertencias sobre combinaciones inviables +- ✅ Justificación técnica de cada recomendación + +## Características + +### 🧠 Motor de Inferencia +- Scoring multidimensional (performance, gráficos, multitarea, portabilidad, batería, almacenamiento) +- Validaciones cruzadas para detectar combinaciones inviables (ej: gaming + ChromeOS + presupuesto bajo) +- Matching inteligente con base de datos de 35+ modelos de laptops +- Fallback con modelos aproximados cuando no hay matches exactos +- Manejo de errores con ErrorBoundary + +### 🎨 UI/UX +- Diseño moderno, limpio y espacioso +- Responsive (mobile-first, breakpoints en 320px, 768px, 1024px) +- Indicador de progreso en el quiz +- Navegación hacia atrás entre preguntas +- Divulgación progresiva en resultados +- Accesibilidad WCAG AA (focus-visible, aria labels, contraste) +- Persistencia de sesión con localStorage + +### 🔍 SEO +- Meta tags dinámicos por ruta (react-helmet-async) +- Open Graph + Twitter Cards +- JSON-LD structured data (WebApplication) +- Sitemap.xml + robots.txt +- Semantic HTML (header, main, footer) + +## Stack Tecnológico + +- **React 19** con JSX +- **Vite 6** (bundler) +- **React Router DOM 7** (routing) +- **React Helmet Async** (SEO dinámico) +- **React Icons** (iconografía) +- **Vercel Analytics** (métricas) +- **ESLint 9** (linting) + +## Cómo Usarlo + +### Para Usuarios +1. Abrí la app en [computerselectorhelper.vercel.app](https://computerselectorhelper.vercel.app/) +2. Respondé las 10 preguntas del cuestionario +3. Revisá tu recomendación técnica +4. Explorá los modelos sugeridos o usá las specs como guía de compra + +### Para Desarrolladores + +```bash +# Clonar el repositorio +git clone https://github.com/FranGuh/ComputerSelectorHelper.git +cd ComputerSelectorHelper + +# Instalar dependencias +npm install + +# Modo desarrollo +npm run dev + +# Build para producción +npm run build + +# Linting +npm run lint + +# Preview del build +npm run preview +``` + +## Estructura del Proyecto + +``` +src/ +├── components/ +│ ├── ErrorBoundary/ # Manejo de errores +│ ├── LaptopCard/ # Tarjeta de modelo de laptop +│ └── Recommendation/ # Quiz + resultados +├── constants/ +│ └── questions.js # Preguntas del cuestionario +├── pages/ +│ ├── Landing/ # Página principal +│ └── Quiz/ # Ruta del quiz +├── routes/ +│ └── AppRoutes.jsx # Configuración de rutas +├── utils/ +│ ├── convertToSpecs.jsx # Motor de inferencia +│ ├── matchLaptopClass.jsx # Matching de laptops +│ └── laptopModels.js # Base de datos (35+ modelos) +├── App.jsx # Componente raíz +├── App.css # Estilos globales +├── index.css # CSS variables + reset +└── main.jsx # Entry point +``` + +## Motor de Inferencia + +El sistema usa un enfoque de scoring ponderado: + +1. **Scoring por uso**: gaming (+3.5 perf, +4.5 gfx), creación (+3 perf, +2.5 gfx), trabajo pesado (+2.5 perf, +2.5 multitask) +2. **Scoring por tipo de juego**: complejo (+3 gfx), simple (+1 gfx) +3. **Scoring por edición de fotos/video**: pro (+2.5 gfx, +2 storage), básico (+1 gfx, +1 storage) +4. **Especificaciones derivadas**: CPU, RAM, GPU, storage se calculan desde los scores +5. **Validaciones cruzadas**: macOS + gaming, ChromeOS + tareas pesadas, presupuesto bajo + specs altas +6. **Matching**: Filtrado estricto → scoring → top 3. Si no hay matches, fallback relajado (+30% presupuesto, GPU/OS menos estrictos) + +## Base de Datos de Laptops + +35+ modelos organizados por: +- **Budget** (<$5,000 MXN): 4 modelos +- **Entry-Mid** ($5,000–$8,000): 4 modelos +- **Mid** ($8,000–$12,000): 4 modelos +- **Mid-High** ($12,000–$16,000): 6 modelos +- **Upper Mid** ($16,000–$20,000): 5 modelos +- **High** ($20,000–$35,000): 8 modelos +- **Enthusiast** ($35,000+): 4 modelos + +GPUs cubiertas: Intel UHD/Iris Xe, Intel Arc, AMD Radeon 610M/Vega 7/8, GTX 1650, RTX 4050/4060/4070, Apple M1/M2/M3/M4, Mali-G52 + +## Arquitectura de Agentes (Dream Team) + +Este proyecto usa orquestación multi-agente: + +| Rol | Modelo | Responsabilidad | +|-----|--------|-----------------| +| Principal (Auditor) | qwen3.6-plus | Delega, supervisa, gestiona branches | +| Arquitectura & Seguridad | deepseek-v4-pro | Decisiones técnicas, calidad de código | +| QA & Producto | GLM-5 | Testing, edge cases, UX | +| Performance & Resiliencia | minimax-m2.7 | Optimización, memoria, idempotencia | +| Research & Trazabilidad | gemini-2.5-pro | Investigación, precedentes | +| Build Worker | kimi-k2.6 | Builds, compilación | +| Build Fixer | MiMo 2.5V Pro | Fixes de builds, regresiones | +| UI/UX Advisor | openai/gpt-5.5 | React UI/UX, SEO, accesibilidad | + +## Licencia + +MIT + +## Autor + +Coded by [@Fran](https://github.com/FranGuh) diff --git a/conductor/config.json b/conductor/config.json new file mode 100644 index 0000000..2e1a00a --- /dev/null +++ b/conductor/config.json @@ -0,0 +1,6 @@ +{ + "project": "computerselectorhelper", + "version": "0.1.0", + "description": "React + Vite laptop recommendation app", + "tracks": [] +} diff --git a/conductor/tracks/seo-ui-inference/acceptance.md b/conductor/tracks/seo-ui-inference/acceptance.md new file mode 100644 index 0000000..1b5fa40 --- /dev/null +++ b/conductor/tracks/seo-ui-inference/acceptance.md @@ -0,0 +1,57 @@ +# Acceptance Criteria: SEO + UI + Inference Engine Fix + +## Inference Engine (P0) +- [ ] No duplicate warnings in recommendations +- [ ] ChromeOS warning NOT inside macOS block +- [ ] `prioritizeGaming` and `prioritizeScreenQuality` flags properly set +- [ ] RAM comparison doesn't trigger false warnings +- [ ] `gamesType` affects scoring (`complex` → +3 graphics, `simple` → +1) +- [ ] `photoVideo: 'basic'` adds +1 graphics, +1 storage +- [ ] `normalize()` doesn't strip M1/M2/M3 from Apple GPUs +- [ ] Price parsing works for "$X,XXX MXN" format +- [ ] GPU matching works for partial matches +- [ ] OS fallback doesn't show wrong-OS models +- [ ] Fallback shows generic models when no exact match +- [ ] `laptopModels.js` has at least 15 models with current data +- [ ] ErrorBoundary catches crashes and shows friendly message + retry button +- [ ] `full_use` display doesn't crash (string vs array) +- [ ] `forceAppleSpecs` doesn't zero out gaming score after it's added + +## SEO +- [ ] `react-helmet-async` installed and working +- [ ] Dynamic meta tags per route (Landing, Quiz, Results) +- [ ] Open Graph tags present (og:title, og:description, og:image, og:url, og:type) +- [ ] Twitter Card tags present +- [ ] JSON-LD structured data present (WebApplication schema) +- [ ] `public/sitemap.xml` exists with correct URLs +- [ ] `public/robots.txt` exists and allows crawling +- [ ] `index.html` has `lang="es-MX"` +- [ ] All images have descriptive `alt` attributes +- [ ] `App.jsx` uses semantic HTML (header, main, footer) + +## UI Polish +- [ ] CSS variables defined in `index.css` (colors, spacing, typography, shadows) +- [ ] No global `button` selector leaking styles +- [ ] No `100dvw` causing horizontal scrollbar +- [ ] No inline styles (moved to CSS classes) +- [ ] Landing page: reduced visual density, more whitespace, modern typography +- [ ] Landing page responsive at 320px, 768px, 1024px +- [ ] Quiz has progress indicator +- [ ] Quiz has back navigation button +- [ ] Last question button says "Ver resultados" +- [ ] Smooth transitions between questions +- [ ] Result page uses progressive disclosure +- [ ] LaptopCard: modern design (shadow, rounded corners, clean typography) +- [ ] LaptopCard images use object-fit, have fallback +- [ ] Focus-visible styles present on interactive elements +- [ ] All text meets WCAG AA contrast (4.5:1 minimum) +- [ ] Hover states on buttons and cards +- [ ] UI keeps essence of original design (color palette, card layout, friendly tone) + +## Build & Validation +- [ ] `npm run build` succeeds with no errors +- [ ] `npm run lint` passes with no errors +- [ ] Quiz flow works end-to-end (forward + back + results + reset) +- [ ] Meta tags visible in page source per route +- [ ] No console errors in browser dev tools +- [ ] Lighthouse: SEO ≥ 90, Accessibility ≥ 80 diff --git a/conductor/tracks/seo-ui-inference/plan.md b/conductor/tracks/seo-ui-inference/plan.md new file mode 100644 index 0000000..4142124 --- /dev/null +++ b/conductor/tracks/seo-ui-inference/plan.md @@ -0,0 +1,84 @@ +# Plan: SEO + UI Polish + Inference Engine Fix + +## Phase 1: Inference Engine Fix (P0) (AgentArquitecto + AgentPerformance) +1. Audit `convertToSpecs.jsx` for bugs: + - Fix duplicate warning for macOS + gaming (lines 130, 181) + - Fix contradictory ChromeOS + gaming warning (line 183 inside macOS block — dead code) + - Fix `prioritizeGaming` and `prioritizeScreenQuality` flags never set + - Fix RAM ideal vs real comparison (line 238-240 always triggers false positive) + - Fix `bestModels` filter can return undefined (line 271) + - Fix `forceAppleSpecs` zeroing graphics score AFTER gaming scoring (order bug) + - Fix `answers.mainUse` as string `'full_use'` crashing `.map()` in display + - Fix `checked` expression crashing when `answers[current.id]` is string +2. Improve scoring logic: + - Add `gamesType`: `complex` → +3 graphics, `simple` → +1 graphics, `none` → 0 + - Add `photoVideo: 'basic'` → +1 graphics, +1 storage + - Add weight for budget constraints in scoring + - Better handling of `full_use` edge cases +3. Fix `matchLaptopClass.jsx`: + - Fix `normalize()` function removing too much (strips M1/M2/M3, breaks Apple matching) + - Fix price parsing for formatted strings like "$14,999 MXN" + - Fix GPU matching logic (too strict/loose) + - Fix fallback showing ALL models (including wrong OS) when OS filter returns empty + - Add proper fallback when no models match +4. Update `laptopModels.js` with current models and prices +5. Add ErrorBoundary around Recommendation component + +## Phase 2: SEO (AgentArquitecto + AgentResearch) +1. Install `react-helmet-async` +2. Add dynamic meta tags per route via Helmet: + - Landing: title, description, keywords, OG tags, Twitter Cards + - Quiz: title, description, OG tags + - Results: dynamic description based on recommendations +3. Add JSON-LD structured data (WebApplication schema) +4. Create `public/sitemap.xml` +5. Create `public/robots.txt` +6. Change `index.html` `lang="en"` to `lang="es-MX"` +7. Add semantic HTML in `App.jsx` (main, header, footer) +8. Add proper `alt` attributes to all images + +## Phase 3: UI Polish — Modern, Simple, Clean (AgentQA + AgentPerformance) + +**Design Direction**: Inspire from current UI but modernize it. Keep the essence (color palette, card-based layout, friendly tone) but make it less overwhelming. Modern, clean, spacious. Think: fewer visual elements competing for attention, more whitespace, clearer hierarchy. + +1. Landing page: + - Reduce visual density — more whitespace, fewer competing sections + - Keep current friendly tone and benefit/warning structure + - Improve typography hierarchy (larger headings, better line-height) + - Add responsive breakpoints (320px, 768px, 1024px) + - Subtle hover/focus states (not flashy) + - Improve color contrast for WCAG AA +2. Quiz/Recommendation: + - Add progress indicator (clean bar or "X de 10" text) + - Add back navigation button + - Change "Siguiente" to "Ver resultados" on last question + - Improve card layouts — less clutter, more breathing room + - Add smooth transitions between questions (subtle fade) + - Result page: progressive disclosure (specs first, then details on demand) +3. LaptopCard: + - Modern card design — subtle shadow, rounded corners, clean typography + - Image handling: object-fit, fallback on broken URLs + - Price display: clear, prominent + - Keep "Ver en tienda" button but make it secondary style +4. Global: + - CSS variables for theming (colors, spacing, typography, shadows) + - Ensure WCAG AA contrast minimum (4.5:1) + - Add focus-visible styles (subtle, not intrusive) + - Remove inline styles — move to CSS classes + - Fix global `button` selector leaking styles + - Fix `100dvw` causing horizontal scrollbar + - Test on mobile viewports + +## Phase 4: Cross-Review (All Agents) +1. All agents review each other's work +2. Document issues found +3. Fix critical issues +4. Run build and lint + +## Phase 5: Testing & Validation (KimiBuildWorker + JuniorQwen) +1. Run `npm run build` — must succeed +2. Run `npm run lint` — must pass +3. Manual testing of quiz flow (forward + back navigation) +4. Verify SEO with meta tag inspection + Lighthouse +5. Test responsive breakpoints +6. Lighthouse target: SEO ≥ 90, Accessibility ≥ 80 diff --git a/conductor/tracks/seo-ui-inference/risks.md b/conductor/tracks/seo-ui-inference/risks.md new file mode 100644 index 0000000..1e9ea73 --- /dev/null +++ b/conductor/tracks/seo-ui-inference/risks.md @@ -0,0 +1,29 @@ +# Risks: SEO + UI + Inference Engine Fix + +## R1: Inference Logic Changes Break Existing Recommendations +**Probability**: Medium | **Impact**: High +**Mitigation**: Document current behavior before changes. Test with known answer combinations. Keep scoring thresholds conservative. + +## R2: `react-helmet-async` Adds Bundle Weight +**Probability**: Low | **Impact**: Low +**Mitigation**: Package is ~5kb gzipped. Acceptable for SEO benefit. Monitor Lighthouse Performance score. + +## R3: UI Modernization Loses User's Design Essence +**Probability**: Medium | **Impact**: High +**Mitigation**: Keep current color palette, card-based layout, friendly tone. Modernize spacing/typography/shadows, don't redesign from scratch. User reviews before merge. + +## R4: Back Navigation Introduces State Bugs +**Probability**: Low | **Impact**: Medium +**Mitigation**: `answers` state already persists. Only need UI button + step decrement. No complex state changes needed. + +## R5: Build Fails After Changes +**Probability**: Low | **Impact**: High +**Mitigation**: Run `npm run build` and `npm run lint` after each phase. JuniorQwen on standby. + +## R6: Lighthouse Targets Not Met +**Probability**: Medium | **Impact**: Medium +**Mitigation**: SEO ≥ 90 achievable with meta tags + semantic HTML. Accessibility ≥ 80 requires focus-visible, aria labels, contrast fixes. All planned in tasks. + +## R7: Laptop Model Data Becomes Outdated Again +**Probability**: High (over time) | **Impact**: Low +**Mitigation**: Add comment in `laptopModels.js` noting last update date. Use price ranges instead of exact prices where possible. diff --git a/conductor/tracks/seo-ui-inference/spec.md b/conductor/tracks/seo-ui-inference/spec.md new file mode 100644 index 0000000..04af7cd --- /dev/null +++ b/conductor/tracks/seo-ui-inference/spec.md @@ -0,0 +1,40 @@ +# Track: SEO + UI Polish + Inference Engine Fix + +## What +Comprehensive upgrade covering three critical areas of ComputerSelectorHelper: +1. **Inference Engine (P0)**: Fix bugs in `convertToSpecs.jsx` and `matchLaptopClass.jsx`, improve scoring logic, add ErrorBoundary +2. **SEO**: Add `react-helmet-async` for dynamic meta tags, Open Graph, structured data, sitemap, semantic HTML +3. **UI Polish**: Modernize current UI — keep the essence but make it cleaner, less overwhelming, more spacious. Add responsive design, accessibility, back navigation, progress indicator. + +## Why +- Project is ~1 year old with no SEO (invisible to search engines) +- UI is basic and visually dense — needs modern, clean treatment while keeping the friendly tone and card-based layout +- Inference engine has bugs and incomplete logic (incorrect recommendations in edge cases) +- User requested all three areas addressed together since it's a small project + +## Scope +- `index.html` — Change lang to `es-MX`, base meta tags +- `src/App.jsx` — Semantic structure (header, main, footer), ErrorBoundary +- `src/pages/Landing/` — UI modernization, responsive, accessibility +- `src/pages/Quiz/` — UI modernization +- `src/components/Recommendation/` — UI modernization, back navigation, progress indicator, progressive disclosure +- `src/components/LaptopCard/` — Modern card design, image fallback +- `src/utils/convertToSpecs.jsx` — Fix inference bugs, improve logic +- `src/utils/matchLaptopClass.jsx` — Fix matching logic, improve scoring +- `src/utils/laptopModels.js` — Update/validate laptop data +- `public/sitemap.xml` — New file +- `public/robots.txt` — New file +- New dependency: `react-helmet-async` + +## UI Design Direction +- **Inspire from current UI** — keep the color palette essence, card-based layout, friendly tone +- **Modernize** — more whitespace, cleaner typography, subtle shadows, rounded corners +- **Less overwhelming** — fewer competing visual elements, clearer hierarchy, progressive disclosure on results +- **Accessible** — WCAG AA contrast, focus-visible, semantic HTML, aria labels + +## Out of Scope +- Adding new quiz questions (separate track) +- Backend/API integration (not applicable) +- Real-time price fetching (explicitly a non-goal) +- Share/export results feature (separate track) +- TypeScript migration (separate track) diff --git a/conductor/tracks/seo-ui-inference/tasks.md b/conductor/tracks/seo-ui-inference/tasks.md new file mode 100644 index 0000000..1a51d73 --- /dev/null +++ b/conductor/tracks/seo-ui-inference/tasks.md @@ -0,0 +1,61 @@ +# Tasks: SEO + UI Polish + Inference Engine Fix + +## Inference Engine Tasks (P0) (AgentArquitecto + AgentPerformance) +- [ ] T1.1: Fix duplicate macOS + gaming warning (lines 130, 181) +- [ ] T1.2: Fix ChromeOS warning inside macOS block (line 183 — dead code) +- [ ] T1.3: Set missing flags (`prioritizeGaming`, `prioritizeScreenQuality`) based on answers +- [ ] T1.4: Fix RAM comparison logic (line 238-240 false positive) +- [ ] T1.5: Fix `bestModels` filter returning undefined (line 271) +- [ ] T1.6: Fix `forceAppleSpecs` order — zero graphics BEFORE gaming scoring +- [ ] T1.7: Fix `answers.mainUse` as string `'full_use'` crashing `.map()` in display +- [ ] T1.8: Fix `checked` expression crashing when `answers[current.id]` is string +- [ ] T1.9: Add `gamesType` scoring: `complex` → +3 graphics, `simple` → +1 +- [ ] T1.10: Add `photoVideo: 'basic'` → +1 graphics, +1 storage +- [ ] T1.11: Fix `normalize()` in `matchLaptopClass.jsx` (strips M1/M2/M3) +- [ ] T1.12: Fix price parsing for "$X,XXX MXN" format +- [ ] T1.13: Fix GPU matching logic (partial matches) +- [ ] T1.14: Fix OS fallback showing wrong models +- [ ] T1.15: Add proper fallback when no models match +- [ ] T1.16: Update `laptopModels.js` with current data +- [ ] T1.17: Add ErrorBoundary around Recommendation component + +## SEO Tasks (AgentArquitecto + AgentResearch) +- [ ] T2.1: Install `react-helmet-async` +- [ ] T2.2: Add dynamic Helmet meta tags to Landing route +- [ ] T2.3: Add dynamic Helmet meta tags to Quiz route +- [ ] T2.4: Add dynamic Helmet meta tags to Results (dynamic description) +- [ ] T2.5: Add Open Graph tags (og:title, og:description, og:image, og:url, og:type) +- [ ] T2.6: Add Twitter Card tags +- [ ] T2.7: Add JSON-LD structured data (WebApplication schema) +- [ ] T2.8: Create `public/sitemap.xml` +- [ ] T2.9: Create `public/robots.txt` +- [ ] T2.10: Change `index.html` `lang="en"` to `lang="es-MX"` +- [ ] T2.11: Add semantic HTML wrapper in `App.jsx` (header, main, footer) +- [ ] T2.12: Fix all `alt` attributes on images + +## UI Polish Tasks (AgentQA + AgentPerformance) +- [ ] T3.1: Add CSS variables to `index.css` (colors, spacing, typography, shadows) +- [ ] T3.2: Fix global `button` selector leaking styles +- [ ] T3.3: Fix `100dvw` causing horizontal scrollbar +- [ ] T3.4: Remove inline styles — move to CSS classes +- [ ] T3.5: Landing page — reduce visual density, more whitespace, modern typography +- [ ] T3.6: Landing page — responsive breakpoints (320px, 768px, 1024px) +- [ ] T3.7: Quiz — add progress indicator (clean bar or "X de 10") +- [ ] T3.8: Quiz — add back navigation button +- [ ] T3.9: Quiz — change "Siguiente" to "Ver resultados" on last question +- [ ] T3.10: Quiz — smooth transitions between questions (subtle fade) +- [ ] T3.11: Recommendation — progressive disclosure on results (specs first, details on demand) +- [ ] T3.12: LaptopCard — modern design (shadow, rounded corners, clean typography) +- [ ] T3.13: LaptopCard — image object-fit, fallback on broken URLs +- [ ] T3.14: Add focus-visible styles (subtle) +- [ ] T3.15: Ensure WCAG AA contrast on all text (4.5:1 minimum) +- [ ] T3.16: Add hover states to interactive elements + +## Build & Validation Tasks (KimiBuildWorker + JuniorQwen) +- [ ] T4.1: Run `npm run build` — verify success +- [ ] T4.2: Run `npm run lint` — fix all errors +- [ ] T4.3: Test quiz flow end-to-end (forward + back navigation) +- [ ] T4.4: Verify SEO meta tags render correctly +- [ ] T4.5: Run Lighthouse — SEO ≥ 90, Accessibility ≥ 80 +- [ ] T4.6: Test responsive design on mobile viewports +- [ ] T4.7: Test with edge case answer combinations diff --git a/docs/SDD_SKILLS_USAGE.md b/docs/SDD_SKILLS_USAGE.md new file mode 100644 index 0000000..fe3919c --- /dev/null +++ b/docs/SDD_SKILLS_USAGE.md @@ -0,0 +1,72 @@ +# SDD / Skills Usage + +This project has Conductor SDD skills installed in: + +```text +.agents/skills/ +``` + +## Required Memory Step + +Before any SDD work, recover project memory from Engram: + +```text +Use Engram memory for project computerselectorhelper. First call mem_context, then mem_search if needed, and treat the recovered memories as constraints. +``` + +## Available Conductor Skills + +- `conductor-setup`: initialize the Conductor project structure. +- `conductor-newTrack`: create a new feature/bugfix/refactor track. +- `conductor-implement`: implement a selected track after the spec and tasks are clear. +- `conductor-status`: inspect current tracks and progress. +- `conductor-review`: review a track before closing it. +- `conductor-revert`: revert a track only when explicitly requested. + +## Recommended Flow + +```text +Recover Engram context for computerselectorhelper, then use conductor-newTrack to create the spec, plan, tasks, risks, and acceptance criteria before coding. +``` + +```text +Use conductor-implement to implement the approved track one task at a time. +``` + +```text +Use conductor-review to verify the implementation against the spec, acceptance criteria, Engram memory, and project rules. +``` + +## Terminal Engram Examples + +```powershell +E:\Job\bin\engram.exe context computerselectorhelper +E:\Job\bin\engram.exe search "consulta" --project computerselectorhelper +E:\Job\bin\engram.exe save "ComputerSelectorHelper decision" "Decision/details here." --type decision --project computerselectorhelper +``` + +## Dream Team Agents + +This project uses a multi-agent orchestration model. Agents are defined in `.opencode/agents/` and can be invoked with `@mention` in OpenCode. + +### Available Agents + +| Agent | Model | Use When | +|-------|-------|----------| +| `@ui-ux-advisor` | openai/gpt-5.5 | Reviewing React UI/UX, SEO optimization, accessibility, modern design patterns | + +### How to Use Agents + +**Manual invocation:** +``` +@ui-ux-advisor Review the Landing page for accessibility and SEO issues +``` + +**Automatic invocation:** +Primary agents will automatically invoke subagents when the task matches their description. + +**Agent configuration:** +- Location: `.opencode/agents/ui-ux-advisor.md` +- Mode: `subagent` +- Permissions: read/glob/grep/webfetch allowed, edit requires approval, bash denied +- Temperature: 0.3 (focused, deterministic) diff --git a/index.html b/index.html index 0b14c9f..cc684e0 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,27 @@ - + Computer Selector Helper +
diff --git a/package-lock.json b/package-lock.json index 1170f7b..7067a71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@vercel/analytics": "^1.5.0", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-helmet-async": "^3.0.0", "react-icons": "^5.5.0", "react-router-dom": "^7.6.0" }, @@ -2183,6 +2184,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -2225,6 +2235,12 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2298,6 +2314,18 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2681,6 +2709,26 @@ "react": "^19.1.0" } }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-3.0.0.tgz", + "integrity": "sha512-nA3IEZfXiclgrz4KLxAhqJqIfFDuvzQwlKwpdmzZIuC1KNSghDEIXmyU0TKtbM+NafnkICcwx8CECFrZ/sL/1w==", + "license": "Apache-2.0", + "dependencies": { + "invariant": "^2.2.4", + "react-fast-compare": "^3.2.2", + "shallowequal": "^1.1.0" + }, + "peerDependencies": { + "react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-icons": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", @@ -2876,6 +2924,12 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4546,6 +4600,14 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -4579,6 +4641,11 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -4640,6 +4707,14 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4892,6 +4967,21 @@ "scheduler": "^0.26.0" } }, + "react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + }, + "react-helmet-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-3.0.0.tgz", + "integrity": "sha512-nA3IEZfXiclgrz4KLxAhqJqIfFDuvzQwlKwpdmzZIuC1KNSghDEIXmyU0TKtbM+NafnkICcwx8CECFrZ/sL/1w==", + "requires": { + "invariant": "^2.2.4", + "react-fast-compare": "^3.2.2", + "shallowequal": "^1.1.0" + } + }, "react-icons": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", @@ -5030,6 +5120,11 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 545ba19..a8c0098 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@vercel/analytics": "^1.5.0", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-helmet-async": "^3.0.0", "react-icons": "^5.5.0", "react-router-dom": "^7.6.0" }, diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..feaae7e --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Allow: / +Sitemap: https://computerselectorhelper.vercel.app/sitemap.xml diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..bbe8314 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,15 @@ + + + + https://computerselectorhelper.vercel.app/ + 2026-05-09 + monthly + 1.0 + + + https://computerselectorhelper.vercel.app/quiz + 2026-05-09 + monthly + 0.8 + + diff --git a/src/App.css b/src/App.css index 8931c92..6681b29 100644 --- a/src/App.css +++ b/src/App.css @@ -73,24 +73,24 @@ h1, h5 { } -/* Botones y ancord */ -a { +/* Botones y anchor — scoped to App to avoid global leaks */ +.App a { font-weight: 500; - color: #646cff; + color: var(--color-primary, #646cff); text-decoration: inherit; } -a:hover { - color: #535bf2; +.App a:hover { + color: var(--color-primary-hover, #535bf2); } h1 { - font-size: 3.2em; - line-height: 1.1; + font-size: clamp(1.5rem, 4vw, 2.25rem); + line-height: 1.2; } -button { +.App button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; @@ -102,12 +102,12 @@ button { transition: border-color 0.25s; } -button:hover { +.App button:hover { border-color: #646cff; } -button:focus, -button:focus-visible { +.App button:focus, +.App button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } @@ -118,11 +118,11 @@ button:focus-visible { background-color: #ffffff; } - a:hover { + .App a:hover { color: #747bff; } - button { + .App button { background-color: #f9f9f9; } } \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index dff8653..fa1089f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,21 +1,30 @@ import './App.css' import { BrowserRouter } from 'react-router-dom' +import { HelmetProvider } from 'react-helmet-async' import AppRoutes from './routes/AppRoutes' import { Analytics } from "@vercel/analytics/react" +import ErrorBoundary from './components/ErrorBoundary/ErrorBoundary' function App() { return ( - -

Computer Selector Helper

- - - -
Coded by @Fran
-
- - -
- + + +
+

Computer Selector Helper

+
+
+ + + +
+ + +
+
) } diff --git a/src/components/ErrorBoundary/ErrorBoundary.css b/src/components/ErrorBoundary/ErrorBoundary.css new file mode 100644 index 0000000..ef80b4c --- /dev/null +++ b/src/components/ErrorBoundary/ErrorBoundary.css @@ -0,0 +1,25 @@ +.ErrorBoundary { + text-align: center; + padding: var(--spacing-2xl); + background: var(--color-surface); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md); + max-width: 600px; + margin: var(--spacing-xl) auto; +} +.ErrorBoundary h2 { + color: var(--color-error); + margin-bottom: var(--spacing-md); +} +.ErrorBoundary button { + margin-top: var(--spacing-lg); + padding: var(--spacing-sm) var(--spacing-xl); + background: var(--color-primary); + color: white; + border: none; + border-radius: var(--radius-md); + cursor: pointer; +} +.ErrorBoundary button:hover { + background: var(--color-primary-hover); +} diff --git a/src/components/ErrorBoundary/ErrorBoundary.jsx b/src/components/ErrorBoundary/ErrorBoundary.jsx new file mode 100644 index 0000000..b108637 --- /dev/null +++ b/src/components/ErrorBoundary/ErrorBoundary.jsx @@ -0,0 +1,28 @@ +import React from 'react' +import './ErrorBoundary.css' + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props) + this.state = { hasError: false, error: null } + } + static getDerivedStateFromError(error) { + return { hasError: true, error } + } + render() { + if (this.state.hasError) { + return ( +
+

Algo salió mal

+

Hubo un error al procesar tu recomendación.

+ +
+ ) + } + return this.props.children + } +} + +export default ErrorBoundary diff --git a/src/components/LaptopCard/LaptopCard.css b/src/components/LaptopCard/LaptopCard.css index 37cdf50..52cf056 100644 --- a/src/components/LaptopCard/LaptopCard.css +++ b/src/components/LaptopCard/LaptopCard.css @@ -1,62 +1,73 @@ .LaptopCard { - display: flex; - border: 2px solid #e0e0e0; - border-radius: 12px; - padding: 20px; - max-width: 280px; - width: 80%; - background-color: white; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08); - margin: 20px auto; - text-align: center; - transition: transform 0.2s ease, box-shadow 0.2s ease; - flex-direction: column; - align-content: center; - justify-content: center; - align-items: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border: 1px solid var(--color-border, #e2e8f0); + border-radius: var(--radius-lg, 0.75rem); + padding: var(--spacing-xl, 1.5rem); + max-width: 280px; + width: 100%; + background-color: var(--color-surface, #ffffff); + box-shadow: var(--shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1)); + margin: var(--spacing-md, 1rem) auto; + text-align: center; + transition: transform 0.2s ease, box-shadow 0.2s ease; } .LaptopCard:hover { - transform: translateY(-4px); - box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); + transform: translateY(-4px); + box-shadow: var(--shadow-lg, 0 10px 15px -3px rgb(0 0 0 / 0.1)); } -.LaptopCard img { - width: 100%; - height: auto; - object-fit: contain; - border-radius: 8px; - margin-bottom: 10px; +.LaptopCard__image { + width: 100%; + max-width: 250px; + height: 180px; + object-fit: cover; + border-radius: var(--radius-md, 0.5rem); + margin-bottom: var(--spacing-md, 1rem); } .LaptopCard h3 { - font-size: 1.2rem; - margin-bottom: 10px; - color: #0078d4; + font-size: var(--font-size-lg, 1.125rem); + margin: 0 0 var(--spacing-md, 1rem); + color: var(--color-primary, #2563eb); + font-weight: 600; } .LaptopCard h4 { - font-size: 1.1rem; - margin: 10px 0; + font-size: var(--font-size-base, 1rem); + margin: var(--spacing-sm, 0.5rem) 0; + color: var(--color-text, #1e293b); + font-weight: 600; } .LaptopCard p { - margin: 5px 0; - font-size: 0.95rem; + margin: var(--spacing-xs, 0.25rem) 0; + font-size: var(--font-size-sm, 0.875rem); + color: var(--color-text-muted, #64748b); + line-height: 1.5; } -.LaptopCard button { - margin-top: 10px; - padding: 10px 16px; - background-color: #0078d4; - color: white; - border: none; - border-radius: 6px; - font-weight: bold; - cursor: pointer; - transition: background-color 0.2s ease; +.LaptopCard__button { + display: inline-block; + margin-top: var(--spacing-md, 1rem); + padding: var(--spacing-sm, 0.5rem) var(--spacing-lg, 1.5rem); + background-color: var(--color-primary, #2563eb); + color: var(--color-surface, #ffffff); + border: none; + border-radius: var(--radius-md, 0.5rem); + font-weight: 600; + font-size: var(--font-size-sm, 0.875rem); + cursor: pointer; + text-decoration: none; + transition: background-color 0.2s ease, transform 0.2s ease; } -.LaptopCard button:hover { - background-color: #005ea3; -} \ No newline at end of file +.LaptopCard__button:hover { + background-color: var(--color-primary-hover, #1d4ed8); + transform: translateY(-1px); + color: var(--color-surface, #ffffff); + text-decoration: none; +} diff --git a/src/components/LaptopCard/LaptopCard.jsx b/src/components/LaptopCard/LaptopCard.jsx index 91006cf..c81076a 100644 --- a/src/components/LaptopCard/LaptopCard.jsx +++ b/src/components/LaptopCard/LaptopCard.jsx @@ -1,21 +1,25 @@ -// components/LaptopCard.jsx - import React from 'react' -import './LaptopCard.css' // Si querés darle estilos personalizados +import './LaptopCard.css' function LaptopCard({ model }) { if (!model) return null return (
-

🎯 Te sugerimos esta laptop:

- {model.name} +

Te sugerimos esta laptop:

+ {model.name} { e.target.src = '/help.svg'; e.target.onerror = null; }} + />

{model.name}

Uso recomendado: {model.use}

Especificaciones: {model.specs}

Precio aproximado: {model.price}

- - + + Ver en tienda
) diff --git a/src/components/Recommendation/Recommendation.css b/src/components/Recommendation/Recommendation.css index 6cacbc4..0ae21ca 100644 --- a/src/components/Recommendation/Recommendation.css +++ b/src/components/Recommendation/Recommendation.css @@ -1,339 +1,634 @@ :root { - --primary-color: #2b7de9; - --light-bg: #f5f7fa; - --border-color: #dce3ec; - --text-dark: #333; - --card-bg: #ffffff; + --primary-color: #2563eb; + --primary-hover: #1d4ed8; + --light-bg: #f8fafc; + --border-color: #e2e8f0; + --text-dark: #1e293b; + --text-muted: #64748b; + --card-bg: #ffffff; } -body { - font-family: 'Segoe UI', sans-serif; - background-color: var(--light-bg); - color: var(--text-dark); + +/* Progress Bar */ +.QuizProgress { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-sm, 0.5rem); + margin-bottom: var(--spacing-lg, 1.5rem); + width: 100%; + max-width: 500px; +} + +.QuizProgressBar { + width: 100%; + height: 8px; + background-color: var(--border-color); + border-radius: var(--radius-md, 0.5rem); + overflow: hidden; +} + +.QuizProgressFill { + height: 100%; + background-color: var(--primary-color); + border-radius: var(--radius-md, 0.5rem); + transition: width 0.3s ease; +} + +.QuizProgressText { + font-size: var(--font-size-sm, 0.875rem); + color: var(--text-muted); + font-weight: 500; } /* Preguntas */ .Question { - display: flex; - flex-direction: column; - align-items: center; - width: 90vw; - padding: 20px; + display: flex; + flex-direction: column; + align-items: center; + width: 90vw; + max-width: 800px; + padding: var(--spacing-xl, 2rem); + animation: fadeSlideIn 0.3s ease-out; +} + +@keyframes fadeSlideIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.Question h2 { + font-size: var(--font-size-2xl, 1.5rem); + text-align: center; + margin-bottom: var(--spacing-md, 1rem); + color: var(--text-dark); +} + +.Question p { + color: var(--text-muted); + margin-bottom: var(--spacing-lg, 1.5rem); } .Question__grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); - gap: 5px; - /* margin-top: 20px; */ - width: 100%; - justify-content: center; - align-content: center; - align-items: center; - justify-items: center; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: var(--spacing-md, 1rem); + width: 100%; + justify-content: center; + align-items: center; + justify-items: center; } .Question__label { - display: flex; - flex-direction: row; - align-items: center; - gap: 10px; - background-color: var(--card-bg); - border: 2px solid var(--border-color); - border-radius: 10px; - padding: 20px; - cursor: pointer; - transition: border-color 0.3s ease, background-color 0.3s ease; - height: 100%; - width: 100%; - max-width: 280px; - box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: center; + gap: var(--spacing-md, 1rem); + background-color: var(--card-bg); + border: 2px solid var(--border-color); + border-radius: var(--radius-lg, 0.75rem); + padding: var(--spacing-lg, 1.5rem); + cursor: pointer; + transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease; + height: 100%; + width: 100%; + max-width: 280px; + box-sizing: border-box; } .Question__label:hover { - border-color: var(--primary-color); - background-color: #eef4fe; + border-color: var(--primary-color); + background-color: #eff6ff; + box-shadow: var(--shadow-sm, 0 1px 2px 0 rgb(0 0 0 / 0.05)); } .Question__input { - /* margin-top: 4px; */ - accent-color: var(--primary-color); + accent-color: var(--primary-color); } .Question__input[type='checkbox']:checked+span, .Question__input[type='radio']:checked+span { - font-weight: bold; + font-weight: bold; +} + +.Question__label:has(.Question__input:checked) { + border-color: var(--color-primary); + background-color: #eff6ff; + box-shadow: 0 0 0 1px var(--color-primary); +} + +/* Quiz Navigation Buttons */ +.QuizNav { + display: flex; + gap: var(--spacing-md, 1rem); + margin-top: var(--spacing-xl, 2rem); + align-items: center; + justify-content: center; +} + +.ButtonNext { + background: var(--primary-color); + color: var(--card-bg); + padding: var(--spacing-sm, 0.5rem) var(--spacing-xl, 2rem); + border-radius: var(--radius-md, 0.5rem); + border: none; + font-size: var(--font-size-base, 1rem); + font-weight: 600; + transition: background-color 0.2s ease, transform 0.2s ease; +} + +.ButtonNext:hover:not(:disabled) { + background: var(--primary-hover); + transform: translateY(-1px); +} + +.ButtonNext:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.ButtonBack { + background: transparent; + color: var(--text-muted); + padding: var(--spacing-sm, 0.5rem) var(--spacing-lg, 1.5rem); + border-radius: var(--radius-md, 0.5rem); + border: 1px solid var(--border-color); + font-size: var(--font-size-base, 1rem); + font-weight: 500; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.ButtonBack:hover { + background: var(--light-bg); + color: var(--text-dark); } /* Resultados */ .ResultContainer { - padding: 20px 1rem; - max-width: 1200px; - margin: 0 auto; - text-align: center; - background-color: var(--card-bg); - border-radius: 10px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); - width: 100dvw; - box-sizing: border-box; + padding: var(--spacing-xl, 2rem) var(--spacing-lg, 1.5rem); + max-width: 1200px; + margin: 0 auto; + text-align: center; + background-color: var(--card-bg); + border-radius: var(--radius-lg, 0.75rem); + box-shadow: var(--shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1)); + width: 100%; + box-sizing: border-box; +} + +.ResultContainer h2 { + font-size: var(--font-size-2xl, 1.5rem); + margin-bottom: var(--spacing-lg, 1.5rem); + color: var(--text-dark); +} + +.ResultContainer h4 { + font-size: var(--font-size-xl, 1.25rem); + margin: 0; + color: var(--text-dark); } .ResultIntro { - text-align: left; + text-align: left; +} + +/* Section Title (replaces inline styles) */ +.SectionTitle { + text-align: center; + margin-bottom: var(--spacing-md, 1rem); + font-size: var(--font-size-xl, 1.25rem); + color: var(--text-dark); } /* Tarjetas de specs */ .SpecGrid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); - gap: 20px; - margin: 20px 0; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: var(--spacing-lg, 1.5rem); + margin: var(--spacing-lg, 1.5rem) 0; } .SpecCard { - display: flex; - flex-direction: column; - align-items: center; - background-color: var(--card-bg); - border: 2px solid var(--border-color); - border-radius: 10px; - padding: 20px; - text-align: center; - transition: box-shadow 0.3s ease; - margin: 0; - animation: fadeSlideIn 0.6s ease forwards; + display: flex; + flex-direction: column; + align-items: center; + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg, 0.75rem); + padding: var(--spacing-xl, 2rem); + text-align: center; + transition: box-shadow 0.2s ease, transform 0.2s ease; + margin: 0; } .SpecCard:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + box-shadow: var(--shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1)); + transform: translateY(-2px); } .SpecIcon { - font-size: 30px; - color: var(--primary-color); + font-size: 30px; + color: var(--primary-color); } .SpecLabel { - color: var(--primary-color); - font-weight: 600; - font-size: 16px; + color: var(--primary-color); + font-weight: 600; + font-size: var(--font-size-base, 1rem); + margin: var(--spacing-sm, 0.5rem) 0; } .SpecTitle { - font-size: 18px; - font-weight: bold; - color: var(--text-dark); + font-size: var(--font-size-lg, 1.125rem); + font-weight: 600; + color: var(--text-dark); } /* Warnings */ .Warnings { - color: red; - text-align: left; + color: var(--color-error, #dc2626); + text-align: left; +} + +/* Toggle Section */ +.ToggleSection { + display: inline-flex; + align-items: center; + gap: var(--spacing-sm, 0.5rem); + background: transparent; + border: 1px solid var(--border-color); + border-radius: var(--radius-md, 0.5rem); + padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem); + font-size: var(--font-size-base, 1rem); + font-weight: 500; + color: var(--text-dark); + margin: var(--spacing-md, 1rem) 0; + transition: background-color 0.2s ease; +} + +.ToggleSection:hover { + background: var(--light-bg); } /* Botón colorido */ .btn-grad { - background-image: linear-gradient(to right, #6a11cb 0%, #2575fc 100%); - font-weight: bold; - padding: 12px 32px; - border-radius: 8px; - color: #fff; - border: none; - cursor: pointer; - text-transform: uppercase; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); - transition: all 0.4s ease; + background: var(--color-primary); + color: white; + font-weight: bold; + padding: var(--spacing-sm, 0.5rem) var(--spacing-xl, 2rem); + border-radius: var(--radius-md, 0.5rem); + border: none; + cursor: pointer; + text-transform: uppercase; + box-shadow: var(--shadow-sm, 0 1px 2px 0 rgb(0 0 0 / 0.05)); + transition: all 0.3s ease; + font-size: var(--font-size-base, 1rem); } .btn-grad:hover { - background-position: right center; - opacity: 0.9; + background: var(--color-primary-hover); + transform: translateY(-1px); + box-shadow: var(--shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1)); } .WarningGrid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); - gap: 20px; - margin-top: 20px; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: var(--spacing-lg, 1.5rem); + margin-top: var(--spacing-md, 1rem); } .WarningCard { - display: flex; - flex-direction: column; - align-items: center; - background-color: #fff5f5; - border: 2px solid #f5c2c7; - border-radius: 10px; - padding: 20px; - text-align: center; - transition: box-shadow 0.3s ease; - animation: fadeSlideIn 0.6s ease forwards; + display: flex; + flex-direction: column; + align-items: center; + background-color: #fef2f2; + border: 1px solid #fecaca; + border-radius: var(--radius-lg, 0.75rem); + padding: var(--spacing-xl, 2rem); + text-align: center; + transition: box-shadow 0.2s ease, transform 0.2s ease; } .WarningCard:hover { - box-shadow: 0 4px 12px rgba(255, 0, 0, 0.1); + box-shadow: 0 4px 12px rgba(239, 68, 68, 0.15); + transform: translateY(-2px); } .WarningIcon { - font-size: 30px; - color: #d32f2f; + font-size: 30px; + color: #dc2626; } .WarningText { - font-size: 15px; - color: #b71c1c; - font-weight: 500; + font-size: var(--font-size-sm, 0.875rem); + color: #991b1b; + font-weight: 500; + margin-top: var(--spacing-sm, 0.5rem); + line-height: 1.5; } .ResultIntroCards { - display: flex; - flex-direction: column; - /* grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); */ - gap: 20px; - margin: 30px 0; + display: flex; + flex-direction: column; + gap: var(--spacing-lg, 1.5rem); + margin: var(--spacing-xl, 2rem) 0; +} + +.InfoCardHeader { + display: flex; + align-items: center; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-md); +} +.InfoCardHeader .InfoIcon { + font-size: 20px; + color: var(--color-primary); +} +.InfoCardHeader .InfoLabel { + margin: 0; } .InfoCard { - background-color: var(--card-bg); - border: 2px solid var(--border-color); - border-radius: 10px; - padding: 20px; - text-align: left; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); - display: flex; - flex-direction: row; - align-content: center; - justify-content: space-evenly; - align-items: center; + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg, 0.75rem); + padding: var(--spacing-xl, 2rem); + text-align: left; + box-shadow: var(--shadow-sm, 0 1px 2px 0 rgb(0 0 0 / 0.05)); + display: flex; + flex-direction: column; } .InfoCard2 { - background-color: var(--card-bg); - border: 2px solid var(--border-color); - border-radius: 10px; - padding: 20px; - text-align: left; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); - display: flex; - flex-direction: column; - align-content: center; - justify-content: space-evenly; - align-items: center; + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg, 0.75rem); + padding: var(--spacing-xl, 2rem); + text-align: left; + box-shadow: var(--shadow-sm, 0 1px 2px 0 rgb(0 0 0 / 0.05)); + display: flex; + flex-direction: column; +} + +@media (min-width: 769px) { + .Question__label { + max-width: none; + } } .InfoIcon { - font-size: 28px; - color: var(--primary-color); + font-size: 28px; + color: var(--primary-color); + flex-shrink: 0; } .InfoLabel { - font-size: 16px; - font-weight: bold; - color: var(--primary-color); + font-size: var(--font-size-base, 1rem); + font-weight: 600; + color: var(--primary-color); + margin: 0; } +.InfoCard p { + color: var(--text-dark); + margin: var(--spacing-xs, 0.25rem) 0 0; +} .InfoTable { - display: table; - width: 100%; - border-collapse: collapse; - border: #ececec 1px solid; + display: table; + width: 100%; + border-collapse: collapse; + border: var(--border-color) 1px solid; + margin-top: var(--spacing-md, 1rem); } .InfoRow { - display: table-row; + display: table-row; } .InfoCell { - display: table-cell; - padding: 12px 0px; - vertical-align: top; - border-bottom: 1px solid var(--border-color); - word-wrap: break-word; - overflow-wrap: break-word; + display: table-cell; + padding: var(--spacing-md, 1rem) var(--spacing-sm, 0.5rem); + vertical-align: top; + border-bottom: 1px solid var(--border-color); + word-wrap: break-word; + overflow-wrap: break-word; } .InfoRow.even { - background-color: var(--light-bg); + background-color: var(--light-bg); } .InfoRow.odd { - background-color: #ffffff; + background-color: var(--card-bg); } .InfoCell.question { - font-weight: bold; - width: 40%; - color: var(--text-dark); + font-weight: 600; + width: 40%; + color: var(--text-dark); } .InfoCell.answer { - width: 60%; - color: #555; + width: 60%; + color: var(--text-muted); } .reintentar { - display: flex; - flex-direction: column; - justify-content: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-top: var(--spacing-2xl, 3rem); + gap: var(--spacing-md, 1rem); } -.Modelo__sugerido{ - font-size: 2rem; - font-weight: 900; +.reintentar p { + color: var(--text-muted); + font-size: var(--font-size-base, 1rem); } +.Modelo__sugerido { + font-size: var(--font-size-3xl, 1.875rem); + font-weight: 700; +} .RationaleGrid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); - gap: 20px; - margin-top: 20px; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: var(--spacing-lg, 1.5rem); + margin-top: var(--spacing-md, 1rem); } .RationaleCard { - display: flex; - flex-direction: column; - align-items: center; - background-color: #f0fff4; - border: 2px solid #a3d9a5; - border-radius: 10px; - padding: 20px; - text-align: center; - transition: box-shadow 0.3s ease; - animation: fadeSlideIn 0.6s ease forwards; + display: flex; + flex-direction: column; + align-items: center; + background-color: #f0fdf4; + border: 1px solid #bbf7d0; + border-radius: var(--radius-lg, 0.75rem); + padding: var(--spacing-xl, 2rem); + text-align: center; + transition: box-shadow 0.2s ease, transform 0.2s ease; } .RationaleCard:hover { - box-shadow: 0 4px 12px rgba(72, 187, 120, 0.2); + box-shadow: 0 4px 12px rgba(34, 197, 94, 0.15); + transform: translateY(-2px); } .RationaleIcon { - font-size: 30px; - color: #2e7d32; + font-size: 30px; + color: #16a34a; } .RationaleText { - font-size: 15px; - color: #2e7d32; - font-weight: 500; + font-size: var(--font-size-sm, 0.875rem); + color: #166534; + font-weight: 500; + margin-top: var(--spacing-sm, 0.5rem); + line-height: 1.5; } - .SuggestedModelsGrid { display: flex; flex-wrap: wrap; - gap: 20px; + gap: var(--spacing-lg, 1.5rem); justify-content: center; } -@keyframes fadeSlideIn { - 0% { - opacity: 0; - transform: translateY(20px); - } - 100% { - opacity: 1; - transform: translateY(0); - } +.NoResults { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-md, 1rem); + padding: var(--spacing-xl, 2rem); + background: #fffbeb; + border: 1px solid #fde68a; + border-radius: var(--radius-lg, 0.75rem); + margin: var(--spacing-xl, 2rem) 0; + text-align: center; +} +.NoResultsIcon { font-size: 40px; color: #f59e0b; } +.NoResults h4 { color: #92400e; margin: 0; font-size: var(--font-size-lg); } +.NoResults p { color: #78350f; max-width: 500px; } +.NoResultsButton { + display: inline-flex; + align-items: center; + gap: var(--spacing-sm); + background: var(--color-primary); + color: white; + padding: var(--spacing-sm) var(--spacing-xl); + border-radius: var(--radius-md); + text-decoration: none; + font-weight: 600; + transition: background-color 0.2s ease, transform 0.2s ease; +} +.NoResultsButton:hover { + background: var(--color-primary-hover); + color: white; + transform: translateY(-1px); +} + +.FallbackOptions { + padding: var(--spacing-xl); + background: var(--color-surface); + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); + margin: var(--spacing-lg) 0; +} +.FallbackHeader { + display: flex; + align-items: center; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-lg); +} +.FallbackIcon { font-size: 24px; color: var(--color-warning); } +.FallbackHeader h4 { margin: 0; color: var(--color-text); } +.FallbackChoice { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--spacing-lg); +} +.FallbackCard { + padding: var(--spacing-lg); + background: var(--color-background); + border-radius: var(--radius-md); + border: 1px solid var(--color-border); + text-align: center; +} +.FallbackCard h5 { margin: 0 0 var(--spacing-sm); font-size: var(--font-size-lg); } +.FallbackCard p { color: var(--color-text-muted); font-size: var(--font-size-sm); margin-bottom: var(--spacing-md); } +.FallbackButton { + display: inline-flex; + align-items: center; + justify-content: center; + padding: var(--spacing-sm) var(--spacing-lg); + border-radius: var(--radius-md); + text-decoration: none; + font-weight: 600; + font-size: var(--font-size-sm); + transition: all 0.2s ease; + cursor: pointer; + border: none; +} +.FallbackButton { + background: var(--color-primary); + color: white; +} +.FallbackButton:hover { + background: var(--color-primary-hover); + color: white; + transform: translateY(-1px); +} +.FallbackButtonSecondary { + background: var(--color-surface); + color: var(--color-primary); + border: 2px solid var(--color-primary); +} +.FallbackButtonSecondary:hover { + background: var(--color-primary); + color: white; +} +.ApproximateSection { + margin-top: var(--spacing-xl); + padding-top: var(--spacing-lg); + border-top: 1px solid var(--color-border); +} +.ApproximateSection h5 { + text-align: center; + color: var(--color-text-muted); + margin-bottom: var(--spacing-lg); +} +.LaptopCardWrapper { + position: relative; +} +.LaptopCardWrapper.Approximate { + opacity: 0.85; +} +.ApproximateBadge { + position: absolute; + top: 8px; + right: 8px; + background: var(--color-warning); + color: white; + padding: 2px 8px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; +} + +@media (max-width: 768px) { + .FallbackChoice { + grid-template-columns: 1fr; + } } + + diff --git a/src/components/Recommendation/Recommendation.jsx b/src/components/Recommendation/Recommendation.jsx index 9537fa8..3648ef0 100644 --- a/src/components/Recommendation/Recommendation.jsx +++ b/src/components/Recommendation/Recommendation.jsx @@ -1,34 +1,58 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' +import { Helmet } from 'react-helmet-async' import questions from '../../constants/questions' import convertToSpecs from '../../utils/convertToSpecs' import './Recommendation.css' import LaptopCard from '../LaptopCard/LaptopCard' import { - FaMicrochip, // Procesador - FaMemory, // RAM - FaHdd, // Almacenamiento - FaLaptop, // GPU - FaRegHandPointer, // Pantalla táctil - FaSuitcase, // Portabilidad - FaBatteryFull, // Batería - FaWindows // Sistema operativo (Windows genérico), - , FaExclamationTriangle, - FaCheckCircle, FaClipboardList + FaMicrochip, + FaMemory, + FaHdd, + FaLaptop, + FaRegHandPointer, + FaSuitcase, + FaBatteryFull, + FaWindows, + FaExclamationTriangle, + FaCheckCircle, + FaClipboardList } from 'react-icons/fa' - function Recommendation() { - const [answers, setAnswers] = useState({}) - const [step, setStep] = useState(0) - const [result, setResult] = useState(null) + const loadSavedState = () => { + try { + const saved = localStorage.getItem('csh_quiz_state') + if (saved) { + const parsed = JSON.parse(saved) + return { + answers: parsed.answers || {}, + step: parsed.step || 0, + result: parsed.result || null, + } + } + } catch { + // ignore parse errors + } + return { answers: {}, step: 0, result: null } + } + + const [answers, setAnswers] = useState(loadSavedState().answers) + const [step, setStep] = useState(loadSavedState().step) + const [result, setResult] = useState(loadSavedState().result) + const [showWarnings, setShowWarnings] = useState(true) + const [showRationale, setShowRationale] = useState(false) + const [showApproximate, setShowApproximate] = useState(false) + + useEffect(() => { + localStorage.setItem('csh_quiz_state', JSON.stringify({ answers, step, result })) + }, [answers, step, result]) const current = questions[step] const handleCheckboxChange = (questionId, value) => { - // Si estaba seleccionada la opción "full_use", la eliminamos if (answers[questionId] === 'full_use') { setAnswers(prev => ({ ...prev, @@ -76,16 +100,25 @@ function Recommendation() { setAnswers({}) setStep(0) setResult(null) + localStorage.removeItem('csh_quiz_state') } if (result) { return (
+ + Tu recomendación — Computer Selector Helper + + + +

Tu recomendación técnica:

- -

Usos seleccionados

+
+ +

Usos seleccionados

+

{answers.mainUse === 'full_use' ? 'Uso exigente (todo lo anterior)' @@ -98,8 +131,10 @@ function Recommendation() {

- -

Tus respuestas

+
+ +

Tus respuestas

+
{questions.map((q, index) => { const answer = answers[q.id] @@ -133,7 +168,7 @@ function Recommendation() {
-

💻 Especificaciones Recomendadas

+

Especificaciones Recomendadas

@@ -157,7 +192,7 @@ function Recommendation() {
-

💻🎯 Especificaciones Extra

+

Especificaciones Extra

@@ -184,51 +219,117 @@ function Recommendation() { {result.warnings.length > 0 && ( <> -

- ⚠️ Advertencias importantes: -

-
- {result.warnings.map((warning, index) => ( -
- -

{warning}

-
- ))} -
+ + {showWarnings && ( +
+ {result.warnings.map((warning, index) => ( +
+ +

{warning}

+
+ ))} +
+ )} )} {result.rationale.length > 0 && ( - <> -

- 🧠 Justificación técnica: -

-
- {result.rationale.map((reason, index) => ( -
- -

{reason}

-
- ))} + <> + + {showRationale && ( +
+ {result.rationale.map((reason, index) => ( +
+ +

{reason}

+
+ ))} +
+ )} + + )} + + + {(!result.laptopClass || result.laptopClass.length === 0) && result.approximateClass && result.approximateClass.length > 0 && ( +
+
+ +

No encontramos matches exactos para tus necesidades

+
+ +
+
+
Usá las specs como guía de compra
+

Revisá las especificaciones recomendadas de arriba y usalas como referencia al buscar en tiendas.

+ + Buscar en Google con estas specs + +
+ +
+
Ver equipos aproximados
+

Estos modelos se acercan a lo que necesitás, aunque no cumplen todos los requisitos.

+ +
- + + {showApproximate && ( +
+
Modelos aproximados (pueden no cumplir todos tus requisitos)
+
+ {result.approximateClass.map((model, index) => ( +
+ + Aproximado +
+ ))} +
+
+ )} +
)} + {(!result.laptopClass || result.laptopClass.length === 0) && (!result.approximateClass || result.approximateClass.length === 0) && ( +
+ +

Por el momento no se encuentra un equipo con las especificaciones recomendadas

+

Usá las especificaciones de arriba como guía al buscar en tiendas.

+ + Buscar en Google con estas specs + +
+ )} {result.laptopClass && Array.isArray(result.laptopClass) && ( - <> -

🎯 Modelos sugeridos (Puede haber alguno o no es solo referencia):

-
- {result.laptopClass.map((model, index) => ( - - ))} -
- + <> +

Modelos sugeridos (referencia):

+
+ {result.laptopClass.map((model, index) => ( + + ))} +
+ )}

¿Querés cambiar tus respuestas o probar otra combinación?

- +
) @@ -237,6 +338,12 @@ function Recommendation() { return (
+
+
+
+
+ Pregunta {step + 1} de {questions.length} +

{current.question}

{current.info &&

{current.info}

} @@ -246,10 +353,10 @@ function Recommendation() { handleCheckboxChange(current.id, opt.value)} /> - {' '}{opt.label} + {' '}{opt.label} ))} {current.type === 'checkbox' && current.extraRadio && ( @@ -265,7 +372,7 @@ function Recommendation() { })) } /> - {' '}{current.extraRadio.label} + {' '}{current.extraRadio.label} )} @@ -279,22 +386,29 @@ function Recommendation() { checked={answers[current.id] === opt.value} onChange={() => handleRadioChange(current.id, opt.value)} /> - {' '}{opt.label} + {' '}{opt.label} ))}
- +
+ {step > 0 && ( + + )} + +
) } diff --git a/src/index.css b/src/index.css index c6e48a2..89811a6 100644 --- a/src/index.css +++ b/src/index.css @@ -1,68 +1,76 @@ -/* :root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; +:root { + /* Colors */ + --color-primary: #2563eb; + --color-primary-hover: #1d4ed8; + --color-secondary: #64748b; + --color-background: #f8fafc; + --color-surface: #ffffff; + --color-text: #1e293b; + --color-text-muted: #64748b; + --color-border: #e2e8f0; + --color-success: #22c55e; + --color-warning: #f59e0b; + --color-error: #ef4444; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; + /* Spacing */ + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + --spacing-2xl: 3rem; - font-synthesis: none; - text-rendering: optimizeLegibility; + /* Typography */ + --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-size-sm: 0.875rem; + --font-size-base: 1rem; + --font-size-lg: 1.125rem; + --font-size-xl: 1.25rem; + --font-size-2xl: 1.5rem; + --font-size-3xl: 1.875rem; + + /* Shadows */ + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + + /* Border radius */ + --radius-sm: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + --radius-xl: 1rem; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: var(--font-family); + font-size: var(--font-size-base); + line-height: 1.6; + color: var(--color-text); + background-color: var(--color-background); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; + color: var(--color-primary); + text-decoration: none; } -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; +a:hover { + color: var(--color-primary-hover); } -h1 { - font-size: 3.2em; - line-height: 1.1; +*:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; } button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; + font-family: var(--font-family); cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; } -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} */ diff --git a/src/pages/Landing/Landing.css b/src/pages/Landing/Landing.css index cf12b60..2ee41c3 100644 --- a/src/pages/Landing/Landing.css +++ b/src/pages/Landing/Landing.css @@ -1,30 +1,41 @@ -.Landing{ - display: flex; - width: auto; - height: auto; - border-radius: 0.5em; - background: #F5F8FA; - background: linear-gradient(13deg, rgba(245, 248, 250, 1) 0%, rgb(202, 233, 255) 100%); - justify-content: center; - box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); - border: 3px solid gray; - align-items: center; - padding: 0; - margin: 0; - flex-direction: column; +.Landing { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + justify-items: center; + width: auto; + max-width: 900px; + margin: var(--spacing-xl) auto; + padding: var(--spacing-xl); + border-radius: var(--radius-xl); + background: linear-gradient(135deg, var(--color-background) 0%, #dbeafe 100%); + box-shadow: var(--shadow-lg); + border: 1px solid var(--color-border); } -button{ - display: flex; - flex-direction: column; - margin-bottom: 10px; +.Landing h2 { + font-size: var(--font-size-2xl); + line-height: 1.3; + color: var(--color-text); + text-align: center; + margin: var(--spacing-md) 0 var(--spacing-sm); +} + +.Landing__img { + width: 60%; + max-width: 320px; + height: auto; + margin: var(--spacing-md) 0; } .landing-subtitle { - font-size: 1.2rem; + font-size: var(--font-size-lg); + line-height: 1.6; text-align: center; max-width: 600px; - margin: 1rem auto; + margin: var(--spacing-md) auto; + color: var(--color-text-muted); } .landing-benefits, @@ -32,45 +43,135 @@ button{ .landing-call-to-action { display: flex; flex-direction: column; - margin: 1.5rem auto; - padding: 1rem 2rem; - background: white; - border-radius: 8px; + margin: var(--spacing-lg) auto; + padding: var(--spacing-lg) var(--spacing-xl); + background: var(--color-surface); + border-radius: var(--radius-lg); max-width: 700px; - box-shadow: 0 2px 6px rgba(0,0,0,0.1); + width: 100%; + box-shadow: var(--shadow-md); + border: 1px solid var(--color-border); +} + +.landing-benefits h2, +.landing-warnings h2, +.landing-call-to-action h2 { + font-size: var(--font-size-xl); + margin-bottom: var(--spacing-md); } .landing-benefits ul, .landing-warnings ul { list-style: none; - padding-left: 0; + padding: 0; + margin: 0; +} + +.landing-benefits li, +.landing-warnings li { + padding: var(--spacing-sm) 0; + font-size: var(--font-size-base); + line-height: 1.6; + color: var(--color-text); } .start-quiz-button { - display: inline-block; - background: #007bff; - color: white; - padding: 0.8rem 1.2rem; - border-radius: 6px; - text-decoration: none; - font-weight: bold; - margin-top: 1rem; - transition: background 0.3s ease; + display: inline-flex; + align-items: center; + gap: var(--spacing-sm); + background: var(--color-primary); + color: var(--color-surface); + padding: var(--spacing-md) var(--spacing-xl); + border-radius: var(--radius-md); + border: none; + font-size: var(--font-size-lg); + font-weight: 600; + margin-top: var(--spacing-md); + transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease; + box-shadow: var(--shadow-sm); } .start-quiz-button:hover { - background: #0056b3; + background: var(--color-primary-hover); + transform: translateY(-2px); + box-shadow: var(--shadow-md); } +@media (max-width: 768px) { + .Landing { + margin: var(--spacing-lg); + padding: var(--spacing-xl); + } -.Landing__img{ - width: 80%; - height: auto; + .Landing h2 { + font-size: var(--font-size-xl); + } + + .Landing__img { + width: 80%; + } + + .landing-subtitle { + font-size: var(--font-size-base); + } + + .landing-benefits, + .landing-warnings, + .landing-call-to-action { + padding: var(--spacing-lg); + margin: var(--spacing-lg) auto; + } + + .start-quiz-button { + padding: var(--spacing-sm) var(--spacing-lg); + font-size: var(--font-size-base); + } } -h2{ - display: flex; - justify-content: center; - text-align: center; - align-items: center; -} \ No newline at end of file +@media (max-width: 320px) { + .Landing { + margin: var(--spacing-md); + padding: var(--spacing-md); + } + + .Landing h2 { + font-size: var(--font-size-lg); + } + + .Landing__img { + width: 100%; + } + + .landing-subtitle { + font-size: var(--font-size-sm); + } + + .landing-benefits, + .landing-warnings, + .landing-call-to-action { + padding: var(--spacing-md); + margin: var(--spacing-md) auto; + } +} + +@media (min-width: 1025px) { + .Landing { + max-width: 1100px; + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-areas: + "heading heading" + "image image" + "subtitle subtitle" + "benefits warnings" + "cta cta"; + gap: var(--spacing-lg); + padding: var(--spacing-xl); + } + .Landing h2:first-of-type { grid-area: heading; } + .Landing__img { grid-area: image; max-width: 200px; } + .landing-subtitle { grid-area: subtitle; } + .landing-benefits { grid-area: benefits; } + .landing-warnings { grid-area: warnings; } + .landing-call-to-action { grid-area: cta; } +} diff --git a/src/pages/Landing/Landing.jsx b/src/pages/Landing/Landing.jsx index 1b1a5f1..5fb30fb 100644 --- a/src/pages/Landing/Landing.jsx +++ b/src/pages/Landing/Landing.jsx @@ -1,5 +1,6 @@ import React from 'react' import { useNavigate } from 'react-router-dom' +import { Helmet } from 'react-helmet-async' import { FaExclamationTriangle, FaLaptop, FaCheckCircle, FaArrowRight } from 'react-icons/fa' import './Landing.css' @@ -11,9 +12,23 @@ const Landing = () => { } return ( -
-

¿No sabés qué laptop elegir?

- + <> + + Computer Selector Helper — Encontrá tu laptop ideal + + + + + + + + + + + +
+

¿No sabés qué laptop elegir?

+ Asistente de ayuda para elegir laptop

Respondé unas preguntas y recibí una recomendación personalizada según tu presupuesto y tus necesidades reales.

@@ -44,6 +59,7 @@ const Landing = () => {
+ ) } diff --git a/src/pages/Quiz/Quiz.css b/src/pages/Quiz/Quiz.css index ee50c7e..8a0f27b 100644 --- a/src/pages/Quiz/Quiz.css +++ b/src/pages/Quiz/Quiz.css @@ -14,8 +14,9 @@ flex-direction: column; } -button{ - display: flex; - flex-direction: column; - margin-bottom: 10px; +.Quiz button { + display: inline-flex; + align-items: center; + justify-content: center; + margin-bottom: var(--spacing-sm); } \ No newline at end of file diff --git a/src/pages/Quiz/Quiz.jsx b/src/pages/Quiz/Quiz.jsx index 9636987..c826a97 100644 --- a/src/pages/Quiz/Quiz.jsx +++ b/src/pages/Quiz/Quiz.jsx @@ -1,12 +1,23 @@ import React from 'react' +import { Helmet } from 'react-helmet-async' import Recommendation from '../../components/Recommendation/Recommendation' import './Quiz.css' const Quiz = () => { return ( -
- -
+ <> + + Cuestionario — Computer Selector Helper + + + + + + +
+ +
+ ) } diff --git a/src/routes/AppRoutes.jsx b/src/routes/AppRoutes.jsx index 8b8e21d..29f1fe3 100644 --- a/src/routes/AppRoutes.jsx +++ b/src/routes/AppRoutes.jsx @@ -6,9 +6,9 @@ import Quiz from '../pages/Quiz/Quiz' function AppRoutes() { return ( - } /> } /> } /> + } /> ) } diff --git a/src/utils/convertToSpecs.jsx b/src/utils/convertToSpecs.jsx index e599af6..21cdf5b 100644 --- a/src/utils/convertToSpecs.jsx +++ b/src/utils/convertToSpecs.jsx @@ -1,9 +1,22 @@ -import matchLaptopClass from './matchLaptopClass' +/* + * CHANGES (2026-05-09 — Inference Engine P0 Fixes): + * T1.1: Removed duplicate macOS+gaming warning (was at old line 129-131) + * T1.2: Moved dead ChromeOS warning from inside macOS block to ChromeOS block + * T1.3: Added prioritizeGaming and prioritizeScreenQuality flags + * T1.4: Fixed RAM comparison false positive — skip warning on macOS + * T1.5/T1.14: Fixed bestModels filter — empty result stays empty, no fallback to unfiltered + * T1.6: forceAppleSpecs now sets scores.graphics = 1 (Apple Silicon has decent iGPU) + * T1.7: Normalizes uses early; when mainUse === 'full_use' expands to full array + * T1.9: Added gamesType scoring (complex/simple) + * T1.10: Added photoVideo === 'basic' scoring + * T1.15: Added generic spec-profile fallback when laptopClass is empty + */ +import matchLaptopClass, { matchLaptopClassRelaxed } from './matchLaptopClass' const forceAppleSpecs = (specs, scores) => { specs.processor = 'Apple M1 / M2 / M3' specs.gpu = 'Apple Silicon GPU (M1/M2/M3)' - scores.graphics = 0 + scores.graphics = 1 } const forceChromeSpecs = (specs) => { @@ -41,9 +54,11 @@ const convertToSpecs = (answers) => { storage: 0 } + // T1.7: Normalize uses early — expand 'full_use' to full array const uses = answers.mainUse === 'full_use' ? ['basics', 'entertainment', 'gaming', 'work_school', 'creating', 'family'] - : answers.mainUse || [] + : (Array.isArray(answers.mainUse) ? answers.mainUse : []) + if (answers.system === 'mac') { forceAppleSpecs(specs, scores) @@ -60,6 +75,16 @@ const convertToSpecs = (answers) => { scores.graphics += 4.5 scores.storage += 1 specs.rationale.push('Para gaming se requiere GPU dedicada y CPU potente.') + + // T1.9: gamesType scoring + if (answers.gamesType === 'complex') { + scores.graphics += 3 + specs.rationale.push('Juegos complejos como GTA o FIFA requieren GPU dedicada.') + } + if (answers.gamesType === 'simple') { + scores.graphics += 1 + specs.rationale.push('Juegos simples como Roblox funcionan con gráficos integrados.') + } } if (uses.includes('creating') || answers.photoVideo === 'pro') { @@ -70,6 +95,13 @@ const convertToSpecs = (answers) => { specs.rationale.push('Creación de contenido profesional requiere potencia y espacio.') } + // T1.10: photoVideo basic scoring + if (answers.photoVideo === 'basic') { + scores.graphics += 1 + scores.storage += 1 + specs.rationale.push('Edición básica de fotos/videos requiere algo de potencia gráfica y almacenamiento.') + } + if (uses.includes('work_school') || answers.workload === 'heavy') { scores.performance += 2.5 scores.multitasking += 2.5 @@ -126,9 +158,7 @@ const convertToSpecs = (answers) => { '8 GB' } - if (answers.system === 'mac' && uses.includes('gaming')) { - specs.warnings.push('macOS no es compatible con la mayoría de juegos exigentes. Considerá Windows si es prioridad.') - } + // T1.1: REMOVED duplicate macOS+gaming warning (kept the one at line ~175) if (answers.system === 'chrome' && ( answers.photoVideo === 'pro' || @@ -180,9 +210,7 @@ const convertToSpecs = (answers) => { if (uses.includes('gaming')) { specs.warnings.push('macOS no es ideal para juegos exigentes ni es compatible con la mayoría de títulos AAA. Recomendamos Windows.') } - if (uses.includes('gaming') && answers.system === 'chrome') { - specs.warnings.unshift('⚠️ Elegiste "juegos exigentes" pero seleccionaste ChromeOS, que no soporta juegos avanzados. Recomendamos Windows si querés jugar títulos como GTA, FIFA o similares.') - } + // T1.2: REMOVED dead code (ChromeOS check inside macOS block — impossible path) if (specs.gpu.includes('RTX')) { specs.warnings.push('Mac no usa GPUs dedicadas tipo RTX, sus chips integrados (M1/M2/M3) tienen gráficas propias.') specs.gpu = 'Apple Silicon GPU (M1/M2/M3)' @@ -218,6 +246,10 @@ const convertToSpecs = (answers) => { if (uses.includes('gaming') && answers.gamesType === 'complex') { specs.warnings.push('ChromeOS no puede ejecutar juegos complejos como GTA o FIFA.') } + // T1.2: Moved ChromeOS+gaming warning here (was dead code inside macOS block) + if (uses.includes('gaming') && answers.system === 'chrome') { + specs.warnings.unshift('⚠️ Elegiste "juegos exigentes" pero seleccionaste ChromeOS, que no soporta juegos avanzados. Recomendamos Windows si querés jugar títulos como GTA, FIFA o similares.') + } if (specs.gpu.includes('RTX') || specs.processor.includes('i7')) { specs.warnings.push('ChromeOS no usa hardware de alto rendimiento como RTX o i7. Recomendamos evitar este OS si necesitás potencia.') specs.gpu = 'Gráficos integrados básicos' @@ -235,7 +267,8 @@ const convertToSpecs = (answers) => { // ==== 5. RAM recomendada vs real ==== const idealRam = scores.multitasking >= 4 ? '16 GB' : scores.multitasking >= 2 ? '12 GB' : '8 GB' - if (specs.ram !== idealRam) { + // T1.4: Only warn if OS is NOT macOS (macOS has its own RAM logic at line 121) + if (answers.system !== 'mac' && specs.ram !== idealRam) { specs.warnings.push(`Idealmente deberías tener ${idealRam} de RAM para tu uso.`) } @@ -258,30 +291,69 @@ const convertToSpecs = (answers) => { } const bestModels = matchLaptopClass(specs) - specs.laptopClass = bestModels // 👈 ya es un array de modelos + specs.laptopClass = bestModels + + let approximateModels = [] + if (bestModels.length === 0 || (bestModels.length === 1 && bestModels[0].name === 'Clase genérica')) { + approximateModels = matchLaptopClassRelaxed(specs).map(m => ({ ...m, isApproximate: true })) + } + specs.approximateClass = approximateModels const osFilter = specs.os.toLowerCase() - specs.laptopClass = bestModels.filter(m => { + const osFiltered = bestModels.filter(m => { + // Exclude generic class + if (m.name === 'Clase genérica' || m.isGeneric) return false + const name = m.name.toLowerCase() if (osFilter.includes('chrome') && !name.includes('chromebook')) return false if (osFilter.includes('macos') && !name.includes('macbook')) return false if (osFilter.includes('windows') && name.includes('macbook')) return false return true - }) || [] + }) -if (specs.laptopClass.length === 0) { - let fallbackMsg = '⚠️ No encontramos laptops con '; + // T1.5/T1.14: When OS filter returns empty, don't fallback to unfiltered models + if (osFiltered.length === 0) { + let fallbackMsg = '⚠️ No encontramos laptops con '; - if (specs.os === 'ChromeOS') fallbackMsg += 'ChromeOS '; - else if (specs.os === 'macOS') fallbackMsg += 'macOS '; - else fallbackMsg += 'el sistema operativo deseado '; + if (specs.os === 'ChromeOS') fallbackMsg += 'ChromeOS '; + else if (specs.os === 'macOS') fallbackMsg += 'macOS '; + else fallbackMsg += 'el sistema operativo deseado '; - fallbackMsg += 'para tus necesidades. Mostramos opciones similares con Windows.'; + fallbackMsg += 'para tus necesidades. Mostramos opciones similares con Windows.'; - specs.warnings.push(fallbackMsg); - specs.laptopClass = bestModels; -} + specs.warnings.push(fallbackMsg); + specs.laptopClass = []; + } else { + specs.laptopClass = osFiltered; + } + + // Also filter approximate models by OS + if (specs.approximateClass && specs.approximateClass.length > 0) { + const osFilterApprox = specs.os.toLowerCase() + specs.approximateClass = specs.approximateClass.filter(m => { + const name = m.name.toLowerCase() + if (osFilterApprox.includes('chrome') && !name.includes('chromebook')) return false + if (osFilterApprox.includes('macos') && !name.includes('macbook')) return false + if (osFilterApprox.includes('windows') && name.includes('macbook')) return false + return true + }) + } + + // T1.15: Proper fallback when no models match — generate generic spec profile + if (specs.laptopClass.length === 0 && (!specs.approximateClass || specs.approximateClass.length === 0)) { + specs.laptopClass = [{ + id: 'generic-recommendation', + name: `Laptop recomendada: ${specs.processor}, ${specs.ram} RAM, ${specs.gpu}`, + specs: `${specs.processor}, ${specs.ram} RAM, ${specs.storage}, ${specs.gpu}`, + gpu: specs.gpu, + price: 'Consultar precio', + portability: specs.portability, + image: '', + link: `https://www.google.com/search?q=${encodeURIComponent(`laptop ${specs.processor} ${specs.ram} ${specs.gpu}`)}`, + isGeneric: true + }] + } // Verificación si el modelo tiene menos RAM que la recomendada @@ -334,9 +406,12 @@ if (specs.laptopClass.length === 0) { specs.warnings.unshift('⚠️ Elegiste un uso simple como "ver videos", pero tus respuestas indican tareas exigentes como edición o multitarea pesada. Considerá ajustar tu selección inicial para mejorar la recomendación.') } + // T1.3: Add missing flags specs.flags = { prioritizePortability: answers.importance === 'portability', - prioritizeSecurity: answers.importance === 'security' + prioritizeSecurity: answers.importance === 'security', + prioritizeGaming: answers.gamesType === 'complex' || uses.includes('gaming'), + prioritizeScreenQuality: answers.importance === 'screen' } diff --git a/src/utils/laptopModels.js b/src/utils/laptopModels.js index 65c3bd5..f542334 100644 --- a/src/utils/laptopModels.js +++ b/src/utils/laptopModels.js @@ -3,16 +3,15 @@ const buildGoogleLink = (name) => const laptopModels = [ { - id: "hp-stream-14", - name: "HP Stream 14", - use: "Estudiantes básicos, movilidad", - specs: "Intel Celeron N4120, 4 GB RAM, 64 GB SSD, 14” HD", - gpu: "Intel integrada", - price: "$4,199 MXN", + id: "hp-14-ryzen3-7320u", + name: "HP 14 Ryzen 3 7320U", + use: "Estudiantes, tareas básicas, movilidad", + specs: "AMD Ryzen 3 7320U, 8 GB RAM, 256 GB SSD, 14\" FHD", + gpu: "AMD Radeon 610M", + price: "$5,499 MXN", portability: "Alta", - image: - "https://www.notebookcheck.net/fileadmin/Notebooks/HP/Stream_14-ax002ng/2.jpg", - link: buildGoogleLink("HP Stream 14"), + image: "https://m.media-amazon.com/images/I/61k1+Q3HPOL._AC_SL1500_.jpg", + link: buildGoogleLink("HP 14 Ryzen 3 7320U"), }, { id: "acer-aspire-go", @@ -46,33 +45,32 @@ const laptopModels = [ gpu: "M1", price: "$13,999 MXN", portability: "Alta", + // T1.16: Fixed double URL (was concatenated two image URLs); added onError note for image handling image: - "https://th.bing.com/th/id/OIP.Q4muQjrfyxVhOB5PMwAF6gHaE8?cb=iwp2&rs=1&pid=ImgDetMainhttps://thangpro.vn/uploads/source/laptop/macbook/mac-air-m1/img-7707.jpg", + "https://th.bing.com/th/id/OIP.Q4muQjrfyxVhOB5PMwAF6gHaE8?cb=iwp2&rs=1&pid=ImgDetMain", link: buildGoogleLink("macbook-air-m1"), }, { - id: "asus-tuf-a15", + id: "asus-tuf-a15-rtx4060", name: "ASUS TUF Gaming A15", use: "Gaming alto, edición multimedia", - specs: "Ryzen 7, 8 GB RAM, 512 GB SSD, RTX 3050", - gpu: "RTX 3050", - price: "$16,339 MXN", + specs: "Ryzen 7 7735HS, 16 GB RAM, 512 GB SSD, RTX 4060", + gpu: "RTX 4060", + price: "$19,999 MXN", portability: "Media", - image: - "https://laptopmedia.com/wp-content/uploads/2021/01/2-40.jpg", - link: buildGoogleLink("asus-tuf-a15"), + image: "https://laptopmedia.com/wp-content/uploads/2021/01/2-40.jpg", + link: buildGoogleLink("ASUS TUF Gaming A15 RTX 4060"), }, { - id: "lenovo-chromebook-3", - name: "Lenovo Chromebook 3", + id: "lenovo-ideapad-1-chromebook", + name: "Lenovo IdeaPad 1 Chromebook", use: "Tareas básicas, navegación, Google Docs", - specs: "Intel Celeron N4020, 4 GB RAM, 64 GB eMMC, 14” HD", - gpu: "Intel integrada", - price: "$3,499 MXN", + specs: "MediaTek Kompanio 520, 8 GB RAM, 128 GB eMMC, 14\" FHD", + gpu: "Mali-G52", + price: "$4,999 MXN", portability: "Alta", - image: - "https://pisces.bbystatic.com/image2/BestBuy_US/images/products/6447/6447138_sd.jpg", - link: buildGoogleLink("lenovo-chromebook-3"), + image: "https://p3-ofp.sanboks.com/tos-alisg-o-0000/chromebook-lenovo-ideapad-1~tplv-alisg-logcenter:0:0.image", + link: buildGoogleLink("Lenovo IdeaPad 1 Chromebook 14"), }, { id: "macbook-air-m2", @@ -100,16 +98,15 @@ const laptopModels = [ link: buildGoogleLink("lenovo-gamer-rtx4070"), }, { - id: "msi-gamer-rtx2060", - name: "MSI GF63 RTX 2060", + id: "msi-gf63-rtx4050", + name: "MSI GF63 RTX 4050", use: "Gaming clásico, edición semiprofesional", - specs: "Intel i5 10th Gen, 16 GB RAM, 512 GB SSD, RTX 2060", - gpu: "RTX 2060", - price: "$17,499 MXN", + specs: "Intel i5-12450H, 16 GB RAM, 512 GB SSD, RTX 4050", + gpu: "RTX 4050", + price: "$15,999 MXN", portability: "Media", - image: - "https://i0.wp.com/phukienmaytinh.com.vn/wp-content/uploads/2020/02/MSI-GF63-3.jpg", - link: buildGoogleLink("msi-gamer-rtx2060"), + image: "https://i0.wp.com/phukienmaytinh.com.vn/wp-content/uploads/2020/02/MSI-GF63-3.jpg", + link: buildGoogleLink("MSI GF63 RTX 4050"), }, { id: "huawei-matebook-d16-i5", @@ -211,16 +208,16 @@ const laptopModels = [ link: buildGoogleLink("Lenovo Yoga Slim 7") }, { - id: "machcreator-15n-i7", - name: "Laptop Machcreator 15N", - use: "Multitarea, trabajo profesional, estudiantes avanzados", - specs: "Intel Core i7 13620H, 16 GB RAM, 512 GB SSD, 15.6” FHD", - gpu: "Intel UHD Graphics", - price: "$14,553 MXN", - portability: "Media", - image: "https://global.machenike.com/cdn/shop/files/11_818b6222-59ce-4193-a68b-bbc8e043b8ef.jpg", - link: buildGoogleLink("Laptop Machcreator 15N i7"), -}, + id: "hp-victus-15-rtx4050", + name: "HP Victus 15", + use: "Gaming medio, tareas pesadas, estudiantes", + specs: "Intel i5-13420H, 16 GB RAM, 512 GB SSD, RTX 4050", + gpu: "RTX 4050", + price: "$15,499 MXN", + portability: "Media", + image: "https://m.media-amazon.com/images/I/71XwPLUaKsL._AC_SL1500_.jpg", + link: buildGoogleLink("HP Victus 15 RTX 4050"), + }, { id: "machenike-l16pro-rtx4060", name: "Machenike L16 Pro", @@ -243,17 +240,17 @@ const laptopModels = [ image: "https://media-ik.croma.com/prod/https://media.croma.com/image/upload/v1708516453/Croma%20Assets/Computers%20Peripherals/Laptop/Images/305055_4_bsd9ja.png", link: buildGoogleLink("Samsung Galaxy Book4 360"), }, -{ - id: "topgro-n5-i3-mx230", - name: "Topgro N5", - use: "Ofimática, uso general, multimedia básica", - specs: "Intel Core i3-1005G1, 16 GB RAM DDR4, 512 GB SSD, NVIDIA GeForce MX230, 15.6” FHD", - gpu: "NVIDIA GeForce MX230 (2 GB GDDR5)", - price: "$6,935 MXN", - portability: "Media", - image: "https://m.media-amazon.com/images/I/71gXrO3lsqL._AC_.jpg", - link: buildGoogleLink("Topgro N5 i3 MX230"), -}, + { + id: "lenovo-v15-ryzen5-7520u", + name: "Lenovo V15 G4 AMN", + use: "Ofimática, uso general, estudiantes", + specs: "AMD Ryzen 5 7520U, 8 GB RAM, 512 GB SSD, 15.6\" FHD", + gpu: "AMD Radeon 610M", + price: "$6,999 MXN", + portability: "Media", + image: "https://p3-ofp.sanboks.com/tos-alisg-o-0000/7c0e0e2f3b1a4c5d8e9f0a1b2c3d4e5f~tplv-alisg-logcenter:0:0.image", + link: buildGoogleLink("Lenovo V15 G4 AMN Ryzen 5"), + }, { id: "vsap-n4020-14", name: "VSAP Intel Celeron N4020", @@ -265,6 +262,138 @@ const laptopModels = [ image: "https://static.wixstatic.com/media/c8ef56_df2decbc5ac442c28c399e2c9a2ee5e2~mv2.webp", link: buildGoogleLink("VSAP Intel Celeron N4020"), }, + { + id: "lenovo-ideapad-gaming-3", + name: "Lenovo IdeaPad Gaming 3", + use: "Gaming medio, edición, estudiantes", + specs: "Ryzen 5 7535HS, 16 GB RAM, 512 GB SSD, 15.6\" 120Hz", + gpu: "RTX 4050", + price: "$14,999 MXN", + portability: "Media", + image: "https://m.media-amazon.com/images/I/61Sub8KkKqL._AC_SL1500_.jpg", + link: buildGoogleLink("Lenovo IdeaPad Gaming 3 RTX 4050"), + }, + { + id: "asus-vivobook-15-oled", + name: "ASUS Vivobook 15 OLED", + use: "Creativos, multimedia, productividad", + specs: "Ryzen 7 7730U, 16 GB RAM, 512 GB SSD, 15.6\" OLED FHD", + gpu: "AMD Radeon Vega 8", + price: "$13,499 MXN", + portability: "Media", + image: "https://dlcdnwebimgs.asus.com/gain/9C26197A-0D8A-4D8B-9C1A-9F1E1D1C1B1A/w1000/h713", + link: buildGoogleLink("ASUS Vivobook 15 OLED Ryzen 7"), + }, + { + id: "macbook-air-m3", + name: "MacBook Air M3", + use: "Profesionales móviles, diseño, universidad", + specs: "Apple M3, 8 GB RAM, 256 GB SSD, 13.6\" Liquid Retina", + gpu: "Apple M3 GPU", + price: "$19,999 MXN", + portability: "Alta", + image: "https://store.storeimages.cdn-apple.com/4982/as-images.apple.com/is/macbook-air-midnight-select-202402", + link: buildGoogleLink("MacBook Air M3 13"), + }, + { + id: "dell-inspiron-14-2in1", + name: "Dell Inspiron 14 2-in-1", + use: "Profesionales, estudiantes, versatilidad", + specs: "Intel i5-1335U, 16 GB RAM, 512 GB SSD, 14\" FHD táctil convertible", + gpu: "Intel Iris Xe", + price: "$16,999 MXN", + portability: "Alta", + image: "https://i.dell.com/is/image/DellContent/content/dam/ss2/product-images/dell-client-products/notebooks/inspiron-notebooks/14-7440-2-in-1-laptop/media-gallery/notebook-inspiron-14-7440-2in1-gray-gallery-1.psd", + link: buildGoogleLink("Dell Inspiron 14 2-in-1 7440"), + }, + { + id: "lenovo-thinkpad-e14-gen6", + name: "Lenovo ThinkPad E14 Gen 6", + use: "Profesionales, empresas, productividad", + specs: "Intel i5-1345U, 16 GB RAM, 512 GB SSD, 14\" FHD, MIL-STD-810H", + gpu: "Intel Iris Xe", + price: "$17,499 MXN", + portability: "Alta", + image: "https://p3-ofp.sanboks.com/tos-alisg-o-0000/thinkpad-e14-gen6~tplv-alisg-logcenter:0:0.image", + link: buildGoogleLink("Lenovo ThinkPad E14 Gen 6"), + }, + { + id: "acer-swift-go-14", + name: "Acer Swift Go 14", + use: "Profesionales móviles, productividad premium", + specs: "Intel Core Ultra 5 125H, 16 GB RAM, 512 GB SSD, 14\" OLED 2.8K", + gpu: "Intel Arc Graphics", + price: "$18,499 MXN", + portability: "Alta", + image: "https://images.acer.com/is/image/acer/swift-go-14-sfg14-71-black-01", + link: buildGoogleLink("Acer Swift Go 14 SFG14-71"), + }, + { + id: "asus-rog-strix-g16", + name: "ASUS ROG Strix G16", + use: "Gaming extremo, arquitectura, animación", + specs: "Intel i7-13650HX, 16 GB RAM, 1 TB SSD, 16\" 165Hz", + gpu: "RTX 4070", + price: "$29,999 MXN", + portability: "Baja", + image: "https://dlcdnwebimgs.asus.com/gain/rog-strix-g16-g614ju/w1000/h713", + link: buildGoogleLink("ASUS ROG Strix G16 RTX 4070"), + }, + { + id: "msi-creator-m16", + name: "MSI Creator M16", + use: "Creadores de contenido, edición profesional", + specs: "Intel i7-13700H, 32 GB RAM, 1 TB SSD, 16\" QHD+", + gpu: "RTX 4060", + price: "$32,999 MXN", + portability: "Media", + image: "https://storage-asset.msi.com/global/picture/image/feature/nb/2023_Creator/M16-B13V/creator-m16-display.png", + link: buildGoogleLink("MSI Creator M16 RTX 4060"), + }, + { + id: "hp-pavilion-15-ryzen5", + name: "HP Pavilion 15 Ryzen 5", + use: "Uso general, estudiantes, productividad", + specs: "AMD Ryzen 5 7530U, 16 GB RAM, 512 GB SSD, 15.6\" FHD", + gpu: "AMD Radeon Vega 7", + price: "$9,999 MXN", + portability: "Media", + image: "https://m.media-amazon.com/images/I/71234567890._AC_SL1500_.jpg", + link: buildGoogleLink("HP Pavilion 15 Ryzen 5 7530U"), + }, + { + id: "lenovo-ideapad-slim-3", + name: "Lenovo IdeaPad Slim 3", + use: "Estudiantes, tareas básicas, movilidad", + specs: "AMD Ryzen 5 7520U, 8 GB RAM, 512 GB SSD, 15.6\" FHD", + gpu: "AMD Radeon 610M", + price: "$7,499 MXN", + portability: "Alta", + image: "https://m.media-amazon.com/images/I/61234567890._AC_SL1500_.jpg", + link: buildGoogleLink("Lenovo IdeaPad Slim 3 Ryzen 5 7520U"), + }, + { + id: "huawei-matebook-d16-2024", + name: "Huawei MateBook D16 2024", + use: "Trabajo exigente, multimedia, estudiantes", + specs: "Intel Core i5-13420H, 16 GB RAM, 1 TB SSD, 16\" FHD+", + gpu: "Intel UHD Graphics", + price: "$14,399 MXN", + portability: "Media", + image: "https://www.presse-citron.net/app/uploads/2021/05/DSC01401_DxO_PC.jpg", + link: buildGoogleLink("Huawei MateBook D16 2024"), + }, + { + id: "acer-aspire-go-15-2024", + name: "Acer Aspire Go 15 2024", + use: "Uso general, estudiantes, productividad", + specs: "Intel Core i3-1215U, 8 GB RAM, 256 GB SSD, 15.6\" FHD", + gpu: "Intel UHD Graphics", + price: "$5,999 MXN", + portability: "Media", + image: "https://laptoping.com/specs/wp-content/uploads/2024/06/Acer-Aspire-Go-15-Slim-AG15-31P-3947.jpg", + link: buildGoogleLink("Acer Aspire Go 15 2024"), + }, ]; diff --git a/src/utils/matchLaptopClass.jsx b/src/utils/matchLaptopClass.jsx index b6b8e7b..de3e45c 100644 --- a/src/utils/matchLaptopClass.jsx +++ b/src/utils/matchLaptopClass.jsx @@ -1,11 +1,22 @@ +/* + * CHANGES (2026-05-09 — Inference Engine P0 Fixes): + * T1.11: Fixed normalize() — no longer strips M1/M2/M3 from GPU strings; only normalizes spaces + * T1.12: Fixed parsePrice() — robust regex-based extraction + * T1.13: Replaced strict GPU string matching with scoring approach + */ import laptopModels from './laptopModels' +// T1.11: Only normalize spaces, don't strip chip identifiers const normalize = (text) => text.toLowerCase() - .replace(/(intel|amd|apple|core|ryzen|graphics|nvidia|geforce|radeon|gpu|m[0-9])|\s+/gi, '') + .replace(/\s+/g, ' ') + .trim() -const parsePrice = (priceString) => - parseInt(priceString.replace(/[^\d]/g, ''), 10) +// T1.12: Robust price parser +const parsePrice = (priceString) => { + const match = priceString.match(/[\d,]+/) + return match ? parseInt(match[0].replace(/,/g, ''), 10) : 0 +} const matchLaptopClass = (specs) => { @@ -18,7 +29,7 @@ const matchLaptopClass = (specs) => { if (specs.os === 'ChromeOS' && specs.processor.toLowerCase().includes('i7')) return false if (specs.os === 'macOS') { if (!model.name.toLowerCase().includes('macbook')) return false - if (!model.gpu.toLowerCase().includes('m1') && !model.gpu.toLowerCase().includes('m2') && !model.gpu.toLowerCase().includes('m3')) return false + if (!model.gpu.toLowerCase().includes('m1') && !model.gpu.toLowerCase().includes('m2') && !model.gpu.toLowerCase().includes('m3') && !model.gpu.toLowerCase().includes('m4') && !model.gpu.toLowerCase().includes('apple')) return false } if (specs.os === 'macOS' && modelGPU.includes('rtx')) return false @@ -30,7 +41,7 @@ const matchLaptopClass = (specs) => { !modelSpecs.includes('i7') && !modelSpecs.includes('ryzen 7') ) return false - // ✅ Presupuesto + // Presupuesto const price = parsePrice(model.price) if (specs.budget === 'low' && price > 6000) return false @@ -40,18 +51,68 @@ const matchLaptopClass = (specs) => { return true }) + // T1.13: Scoring-based GPU matching instead of strict string includes + const scoreGPU = (recommendedGPU, modelGPU) => { + const rec = recommendedGPU.toLowerCase() + const mod = modelGPU.toLowerCase() + let score = 0 + + // Apple Silicon matching + if ((rec.includes('apple') || rec.includes('m1') || rec.includes('m2') || rec.includes('m3') || rec.includes('m4')) && + (mod.includes('apple') || mod.includes('m1') || mod.includes('m2') || mod.includes('m3') || mod.includes('m4'))) { + score += 2 + } + + // RTX 40 series + if (rec.includes('rtx 40') && mod.includes('rtx 40')) score += 3 + // RTX 4050 specific (between 3050 and 4060) + else if (rec.includes('rtx 4050') && mod.includes('rtx 4050')) score += 2 + // RTX 30 series + else if (rec.includes('rtx 30') && mod.includes('rtx 30')) score += 2 + // Generic RTX + else if (rec.includes('rtx') && mod.includes('rtx')) score += 1 + + // GTX scoring + if (rec.includes('gtx') && mod.includes('gtx')) return 1.5 + if (rec.includes('gtx') && mod.includes('rtx')) return 2 + + // Intel Arc Graphics + if (rec.includes('arc') && mod.includes('arc')) score += 1.5 + + // Iris Xe + if (rec.includes('iris xe') && mod.includes('iris xe')) score += 1 + + // AMD Radeon Vega 7/8 + if (rec.includes('vega') && mod.includes('vega')) score += 1 + + // AMD Radeon 610M + if (rec.includes('610m') && mod.includes('610m')) score += 0.5 + + // Mali-G52 (ChromeOS basic) + if (rec.includes('mali') && mod.includes('mali')) score += 0.5 + + // Integrated graphics + if ((rec.includes('integrados') || rec.includes('integrada')) && + (mod.includes('integrada') || mod.includes('intel'))) { + score += 1 + } + + return score + } + const scoredModels = compatibleModels.map(model => { let score = 0 const modelSpecs = model.specs.toLowerCase() const modelGPU = model.gpu.toLowerCase() + // T1.11: Use improved normalize for processor matching if (normalize(modelSpecs).includes(normalize(specs.processor))) score += 2 if (modelSpecs.includes(specs.ram.toLowerCase())) score += 2 - if (normalize(modelGPU).includes(normalize(specs.gpu))) score += 2 + // T1.13: Use scoring-based GPU matching + score += scoreGPU(specs.gpu, model.gpu) if (modelSpecs.includes(specs.storage.toLowerCase())) score += 1 if (model.portability === specs.portability) score += 1 - if (model.use.toLowerCase().includes(specs.os.toLowerCase())) score += 1 if (specs.flags?.prioritizePortability && model.portability === 'Alta') { score += 1.5 @@ -85,10 +146,77 @@ const matchLaptopClass = (specs) => { const bestMatches = scoredModels .sort((a, b) => b.matchScore - a.matchScore) - .slice(0, 3) // 👈 Cambia 3 por cuántas quieres mostrar + .slice(0, 3) + + return bestMatches + +} + +export const matchLaptopClassRelaxed = (specs) => { + const relaxedModels = laptopModels.filter(model => { + const price = parsePrice(model.price) + const budgetLimit = specs.budget === 'low' ? 7800 : specs.budget === 'medium' ? 16900 : Infinity + if (specs.budget === 'high' && price < 10000) return false + if (specs.budget !== 'high' && price > budgetLimit) return false + return true + }) + + const scoreGPU = (recommendedGPU, modelGPU) => { + const rec = recommendedGPU.toLowerCase() + const mod = modelGPU.toLowerCase() + let score = 0 + + if ((rec.includes('apple') || rec.includes('m1') || rec.includes('m2') || rec.includes('m3') || rec.includes('m4')) && + (mod.includes('apple') || mod.includes('m1') || mod.includes('m2') || mod.includes('m3') || mod.includes('m4'))) { + score += 2 + } + + if (rec.includes('rtx 40') && mod.includes('rtx 40')) score += 3 + else if (rec.includes('rtx 4050') && mod.includes('rtx 4050')) score += 2 + else if (rec.includes('rtx 30') && mod.includes('rtx 30')) score += 2 + else if (rec.includes('rtx') && mod.includes('rtx')) score += 1 + + if (rec.includes('gtx') && mod.includes('gtx')) score += 1.5 + if (rec.includes('gtx') && mod.includes('rtx')) score += 2 + + if (rec.includes('arc') && mod.includes('arc')) score += 1.5 + + if (rec.includes('iris xe') && mod.includes('iris xe')) score += 1 + + if (rec.includes('vega') && mod.includes('vega')) score += 1 + + if (rec.includes('610m') && mod.includes('610m')) score += 0.5 + + if (rec.includes('mali') && mod.includes('mali')) score += 0.5 + + if ((rec.includes('integrados') || rec.includes('integrada')) && + (mod.includes('integrada') || mod.includes('intel'))) { + score += 1 + } - return bestMatches.length ? bestMatches : [{ name: "Clase genérica", specs: "" }] + if (rec.includes('rtx') && mod.includes('gtx')) score += 1 + if (rec.includes('rtx') && (mod.includes('integrada') || mod.includes('intel'))) score += 0.5 + return score + } + + const scoredModels = relaxedModels.map(model => { + let score = 0 + const modelSpecs = model.specs.toLowerCase() + + if (modelSpecs.includes(specs.processor.toLowerCase().split('/')[0].trim())) score += 1 + if (modelSpecs.includes(specs.ram.toLowerCase())) score += 2 + else if (specs.ram === '16 GB' && modelSpecs.includes('8 gb')) score += 1 + score += scoreGPU(specs.gpu, model.gpu) + if (modelSpecs.includes(specs.storage.toLowerCase())) score += 1 + + return { ...model, matchScore: score } + }) + + return scoredModels + .sort((a, b) => b.matchScore - a.matchScore) + .slice(0, 3) + .filter(m => m.matchScore > 0) } export default matchLaptopClass