From 96ad13d6a906b3bb57c553edf0e752a4b3104833 Mon Sep 17 00:00:00 2001 From: DMleadgen Date: Mon, 6 Apr 2026 13:42:56 -0600 Subject: [PATCH] deploy: publish unpublished site updates --- .dockerignore | 5 +- .editorconfig | 12 + .gitignore | 12 +- .lighthouserc.js | 57 +- .prettierignore | 15 + .prettierrc.json | 5 + app/about-us/page.tsx | 35 +- app/about/faqs/page.tsx | 128 +- app/about/page.tsx | 17 +- app/admin/calls/[id]/page.tsx | 197 +- app/admin/calls/page.tsx | 139 +- app/admin/layout.tsx | 8 +- app/admin/orders/page.tsx | 6 +- app/admin/page.tsx | 210 +- app/admin/products/page.tsx | 6 +- app/api/admin/calls/[id]/route.ts | 36 +- app/api/admin/calls/route.ts | 38 +- app/api/chat/route.ts | 151 +- app/api/contact/route.test.ts | 78 +- app/api/contact/route.ts | 4 +- app/api/ebay/notifications/route.ts | 48 +- app/api/ebay/search/route.ts | 136 +- .../internal/phone-calls/complete/route.ts | 57 +- .../internal/phone-calls/lead-link/route.ts | 34 +- app/api/internal/phone-calls/shared.ts | 38 +- app/api/internal/phone-calls/start/route.ts | 55 +- app/api/internal/phone-calls/turn/route.ts | 28 +- app/api/livekit/token/route.ts | 21 +- app/api/manuals/[...path]/route.ts | 6 +- app/api/orders/route.ts | 68 +- app/api/products/admin/route.ts | 89 +- app/api/request-indexing/route.ts | 15 +- app/api/request-machine/route.ts | 4 +- app/api/sitemap-submit/route.ts | 18 +- app/api/stripe/checkout/route.ts | 33 +- app/api/stripe/products/route.ts | 10 +- app/api/stripe/test/route.ts | 52 +- app/api/stripe/webhook/route.ts | 57 +- app/api/thumbnails/[...path]/route.ts | 20 +- app/auto-repair/page.tsx | 84 +- app/blog/abandoned-vending-machines/page.tsx | 8 +- .../page.tsx | 18 +- app/blog/reviews/page.tsx | 112 +- app/car-washes/page.tsx | 83 +- app/checkout/cancel/page.tsx | 16 +- app/checkout/success/page.tsx | 20 +- app/community-centers/page.tsx | 83 +- app/contact-us/page.tsx | 34 +- app/dance-studios/page.tsx | 83 +- .../healthy-options/page.tsx | 118 +- app/food-and-beverage/suppliers/page.tsx | 119 +- .../traditional-options/page.tsx | 118 +- app/gyms/page.tsx | 83 +- app/manuals/README.md | 4 +- app/manuals/dashboard/page.tsx | 56 +- app/orders/page.tsx | 6 +- app/privacy-policy/page.tsx | 25 +- app/products/page.tsx | 33 +- app/reviews/page.tsx | 17 +- app/robots.ts | 36 - app/robots.txt/route.ts | 40 + app/seaga-hy900-support/page.tsx | 177 +- app/services/moving/page.tsx | 145 +- app/services/page.tsx | 283 +- app/services/parts/page.tsx | 155 +- app/services/repairs/page.tsx | 136 +- app/services/service-areas/page.tsx | 24 +- app/sign-in/[[...sign-in]]/page.tsx | 17 +- app/stripe-setup/page.tsx | 127 +- app/style-guide/page.tsx | 1559 +++++++--- app/terms-and-conditions/page.tsx | 29 +- .../machines-for-sale/page.tsx | 102 +- app/vending-machines/machines-we-use/page.tsx | 77 +- app/vending-machines/page.tsx | 13 +- app/warehouses/page.tsx | 85 +- artifacts/README.md | 5 + .../backups/app}/page.tsx.backup | 0 .../formatting}/app/[...slug]/page.tsx | 0 .../app/manuals/dashboard/page.tsx | 0 .../backups/formatting}/app/manuals/page.tsx | 0 .../formatting}/app/service-areas/page.tsx | 0 .../app/services/service-areas/page.tsx | 0 .../app/vending-machines-[location]/page.tsx | 0 .../vending-machines/machines-we-use/page.tsx | 0 .../formatting}/components/about-page.tsx | 0 .../formatting}/components/contact-page.tsx | 0 .../components/contact-section.tsx | 0 .../formatting}/components/faq-section.tsx | 0 .../components/features-section.tsx | 0 .../backups/formatting}/components/footer.tsx | 0 .../backups/formatting}/components/header.tsx | 0 .../components/how-it-works-section.tsx | 0 .../components/manuals-page-client.tsx | 0 .../components/product-showcase-section.tsx | 0 .../components/request-machine-section.tsx | 0 .../components/reviews-section.tsx | 0 .../components/service-areas-section.tsx | 0 .../components/services-section.tsx | 0 .../components/vending-machines-showcase.tsx | 0 .../components/who-we-serve-page.tsx | 0 dev.log => artifacts/logs/dev.log | 0 .../reports/business-optimization.json | 0 .../reports/comprehensive-analysis.json | 0 .../reports/comprehensive-json-ld.json | 0 .../reports/comprehensive-optimization.json | 0 .../reports/comprehensive-seo-report.json | 0 .../reports/comprehensive-sitemap.xml | 0 .../reports/final-seo-report.json | 0 .../reports/final-sitemap.xml | 0 .../reports/formatting-report.json | 0 .../reports/json-ld-output.json | 0 .../reports/link-analysis-after.json | 0 .../reports/optimization-report.json | 0 .../reports/optimized-sitemap.xml | 0 .../reports/round2-optimization.json | 0 .../reports/seo-improvement-report.json | 0 .../reports/seo-report.json | 0 .../reports/updated-json-ld.json | 0 components/about-page.tsx | 38 +- components/add-to-cart-button.tsx | 31 +- components/animated-number.tsx | 22 +- components/assistant-avatar.tsx | 4 +- components/author-bio.tsx | 24 +- components/breadcrumbs.tsx | 45 +- components/cart-button.tsx | 12 +- components/cart.tsx | 79 +- components/credit-card-reader-section.tsx | 1 - components/dropdown-page-shell.tsx | 111 + components/error-boundary.tsx | 12 +- components/faq-schema.tsx | 3 - components/faq-section.tsx | 98 +- components/feature-card.tsx | 4 +- components/forms/contact-form.tsx | 115 +- components/forms/form-button.tsx | 21 +- components/forms/form-input.tsx | 13 +- components/forms/form-select.tsx | 18 +- components/forms/form-textarea.tsx | 13 +- components/forms/request-machine-form.tsx | 133 +- components/forms/sms-consent-fields.tsx | 16 +- components/get-free-machine-modal.tsx | 10 +- components/header.tsx | 80 +- components/image-carousel.tsx | 86 +- components/manual-viewer.tsx | 109 +- components/manuals-dashboard-client.tsx | 176 +- components/manuals-page-client.tsx | 508 ++-- components/manuals-page-experience.tsx | 44 +- components/manuals-page-shell.tsx | 5 +- components/mobile-cart-button.tsx | 10 +- components/moving-service-image.tsx | 19 +- components/nap-data.tsx | 14 +- components/order-management.tsx | 262 +- components/order-tracking.tsx | 273 +- components/page-wrapper.tsx | 44 +- components/parts-panel.tsx | 59 +- components/privacy-policy-page.tsx | 177 +- components/product-admin.tsx | 268 +- components/product-card.tsx | 20 +- components/product-grid.tsx | 6 +- components/repairs-page.tsx | 102 +- components/request-machine-section.tsx | 38 +- components/review-schema.tsx | 3 - components/reviews-page.tsx | 8 + components/stripe-status.tsx | 86 +- components/terms-and-conditions-page.tsx | 158 +- components/theme-provider.tsx | 6 +- components/ui/accordion.tsx | 18 +- components/ui/alert-dialog.tsx | 34 +- components/ui/alert.tsx | 30 +- components/ui/aspect-ratio.tsx | 4 +- components/ui/avatar.tsx | 18 +- components/ui/badge.tsx | 26 +- components/ui/breadcrumb.tsx | 38 +- components/ui/button-group.tsx | 30 +- components/ui/button.tsx | 45 +- components/ui/calendar.tsx | 128 +- components/ui/card.tsx | 38 +- components/ui/carousel.tsx | 84 +- components/ui/chart.tsx | 104 +- components/ui/checkbox.tsx | 14 +- components/ui/collapsible.tsx | 4 +- components/ui/command.tsx | 44 +- components/ui/context-menu.tsx | 42 +- components/ui/dialog.tsx | 32 +- components/ui/drawer.tsx | 38 +- components/ui/dropdown-menu.tsx | 42 +- components/ui/empty.tsx | 44 +- components/ui/field.tsx | 104 +- components/ui/form.tsx | 36 +- components/ui/hover-card.tsx | 14 +- components/ui/input-group.tsx | 98 +- components/ui/input-otp.tsx | 28 +- components/ui/input.tsx | 14 +- components/ui/item.tsx | 92 +- components/ui/kbd.tsx | 14 +- components/ui/label.tsx | 12 +- components/ui/menubar.tsx | 54 +- components/ui/navigation-menu.tsx | 44 +- components/ui/pagination.tsx | 38 +- components/ui/popover.tsx | 14 +- components/ui/progress.tsx | 12 +- components/ui/radio-group.tsx | 16 +- components/ui/resizable.tsx | 18 +- components/ui/scroll-area.tsx | 24 +- components/ui/select.tsx | 46 +- components/ui/separator.tsx | 14 +- components/ui/sheet.tsx | 50 +- components/ui/sidebar.tsx | 300 +- components/ui/skeleton.tsx | 6 +- components/ui/slider.tsx | 18 +- components/ui/sonner.tsx | 16 +- components/ui/spinner.tsx | 8 +- components/ui/switch.tsx | 14 +- components/ui/table.tsx | 46 +- components/ui/tabs.tsx | 18 +- components/ui/textarea.tsx | 10 +- components/ui/toast.tsx | 38 +- components/ui/toaster.tsx | 6 +- components/ui/toggle-group.tsx | 24 +- components/ui/toggle.tsx | 26 +- components/ui/tooltip.tsx | 12 +- components/ui/use-mobile.tsx | 6 +- components/ui/use-toast.ts | 50 +- components/vending-machine-card.tsx | 22 +- components/vending-machines-showcase.tsx | 31 +- components/who-we-serve-page.tsx | 256 +- convex/_generated/api.ts | 6 +- convex/_generated/server.ts | 2 +- convex/admin.ts | 48 +- convex/leads.ts | 122 +- convex/manuals.ts | 50 +- convex/orders.ts | 99 +- convex/products.ts | 56 +- convex/schema.ts | 52 +- convex/voiceSessions.ts | 183 +- docs/README.md | 5 + .../operations/COOLIFY_LAUNCH.md | 10 + .../operations/LIGHTHOUSE_TESTING.md | 8 +- R2_SETUP.md => docs/operations/R2_SETUP.md | 21 +- SEO_SETUP.md => docs/operations/SEO_SETUP.md | 7 +- .../operations/SHADCN_MCP_TROUBLESHOOTING.md | 23 +- .../operations/SITE_CONFIGURATION.md | 18 +- .../operations/STAGING_DEPLOYMENT.md | 0 .../reference/.wordpress-removed.md | 12 +- .../reference/COMPONENT_AUDIT.md | 19 +- .../reference/FIXES_COMPLETED.md | 0 FIXES_PLAN.md => docs/reference/FIXES_PLAN.md | 0 .../reference/STYLE_GUIDE.md | 41 +- .../reference/STYLING_STANDARDIZATION.md | 0 eslint.config.mjs | 35 + hooks/use-mobile.ts | 6 +- hooks/use-toast.ts | 50 +- lib/cart/context.tsx | 35 +- lib/clean-wordPress-content.tsx | 1662 ++++++---- lib/convex-config.ts | 4 +- lib/convex-service.ts | 75 +- lib/convex.ts | 113 +- lib/ebay-api.test.ts | 91 +- lib/ebay-api.ts | 246 +- lib/ebay-notifications.ts | 153 +- lib/email.ts | 44 +- lib/ghl.ts | 40 +- lib/google-search-console.ts | 52 +- lib/internal-link-config.js | 433 ++- lib/internal-link-config.ts | 472 +-- lib/internal-links-config.json | 63 +- lib/location-data.ts | 223 +- lib/manuals-catalog.ts | 10 +- lib/manuals-config.ts | 427 +-- lib/manuals-object-storage.ts | 73 +- lib/manuals-site-selection.ts | 9 +- lib/manuals-storage.ts | 15 +- lib/manuals-types.ts | 23 +- lib/manuals.ts | 577 ++-- lib/model-type-mapping.ts | 241 +- lib/parts-lookup.ts | 169 +- lib/phone-calls.ts | 202 +- lib/products/config.ts | 8 +- lib/products/filters.ts | 6 +- lib/products/types.ts | 4 +- lib/seo-config.ts | 50 +- lib/seo-utils.js | 480 +-- lib/seo-utils.ts | 401 +-- lib/server/admin-auth.ts | 20 +- lib/server/contact-submission.ts | 436 +-- lib/site-chat/config.ts | 5 +- lib/site-chat/intents.ts | 18 +- lib/site-chat/rate-limit.ts | 31 +- lib/site-config.ts | 65 +- lib/site-manufacturer-mapping.json | 10 +- lib/sms-compliance.ts | 7 +- lib/stripe/client.ts | 40 +- lib/stripe/products.ts | 72 +- lib/utils.ts | 4 +- lib/voice-assistant/persistence.ts | 86 +- lib/voice-assistant/server.ts | 17 +- lib/voice-assistant/shared.ts | 2 +- lib/voice-assistant/types.ts | 8 +- lib/webhook-client.ts | 16 +- lib/wordpress-content.ts | 252 +- lib/wordpress-data-loader.ts | 82 +- lib/wordpress-data/image-mapping.json | 2 +- lib/wordpress-data/processed-content.json | 16 +- livekit-agent/README.md | 5 + livekit-agent/worker.ts | 164 +- middleware.ts | 18 +- next.config.mjs | 189 +- package.json | 16 +- pnpm-lock.yaml | 2704 ++++++++++++++++- postcss.config.mjs | 2 +- scripts/README.md | 49 +- scripts/check-public-copy.mjs | 13 +- scripts/deploy-readiness.mjs | 81 +- scripts/generate-internal-links.js | 517 ++-- scripts/lighthouse-test.js | 407 +-- .../one-off/modify_hero.py | 0 .../one-off/modify_home_page.py | 0 .../one-off/modify_links.py | 0 .../one-off/modify_parts_page.py | 0 .../one-off/modify_service_areas.py | 0 .../one-off/test-ebay-api.js | 39 +- .../one-off/test-fixes.sh | 0 scripts/seo-internal-link-tool.js | 1076 ++++--- scripts/sync-manuals-to-convex.ts | 59 +- scripts/sync-manuals-to-public.js | 29 +- scripts/sync-thumbnails-to-public.js | 32 +- scripts/upload-to-r2.js | 276 +- styles/globals.css | 8 +- tsconfig.json | 14 +- tsconfig.typecheck.json | 24 + workers/DEPLOYMENT.md | 21 +- workers/README.md | 18 +- workers/api-worker/index.ts | 402 +-- workers/r2-signed-urls/index.ts | 155 +- 333 files changed, 16960 insertions(+), 9789 deletions(-) create mode 100644 .editorconfig create mode 100644 .prettierignore create mode 100644 .prettierrc.json delete mode 100644 app/robots.ts create mode 100644 app/robots.txt/route.ts create mode 100644 artifacts/README.md rename {app => artifacts/backups/app}/page.tsx.backup (100%) rename {.formatting-backups => artifacts/backups/formatting}/app/[...slug]/page.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/app/manuals/dashboard/page.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/app/manuals/page.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/app/service-areas/page.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/app/services/service-areas/page.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/app/vending-machines-[location]/page.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/app/vending-machines/machines-we-use/page.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/components/about-page.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/components/contact-page.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/components/contact-section.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/components/faq-section.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/components/features-section.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/components/footer.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/components/header.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/components/how-it-works-section.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/components/manuals-page-client.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/components/product-showcase-section.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/components/request-machine-section.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/components/reviews-section.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/components/service-areas-section.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/components/services-section.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/components/vending-machines-showcase.tsx (100%) rename {.formatting-backups => artifacts/backups/formatting}/components/who-we-serve-page.tsx (100%) rename dev.log => artifacts/logs/dev.log (100%) rename business-optimization.json => artifacts/reports/business-optimization.json (100%) rename comprehensive-analysis.json => artifacts/reports/comprehensive-analysis.json (100%) rename comprehensive-json-ld.json => artifacts/reports/comprehensive-json-ld.json (100%) rename comprehensive-optimization.json => artifacts/reports/comprehensive-optimization.json (100%) rename comprehensive-seo-report.json => artifacts/reports/comprehensive-seo-report.json (100%) rename comprehensive-sitemap.xml => artifacts/reports/comprehensive-sitemap.xml (100%) rename final-seo-report.json => artifacts/reports/final-seo-report.json (100%) rename final-sitemap.xml => artifacts/reports/final-sitemap.xml (100%) rename formatting-report.json => artifacts/reports/formatting-report.json (100%) rename json-ld-output.json => artifacts/reports/json-ld-output.json (100%) rename link-analysis-after.json => artifacts/reports/link-analysis-after.json (100%) rename optimization-report.json => artifacts/reports/optimization-report.json (100%) rename optimized-sitemap.xml => artifacts/reports/optimized-sitemap.xml (100%) rename round2-optimization.json => artifacts/reports/round2-optimization.json (100%) rename seo-improvement-report.json => artifacts/reports/seo-improvement-report.json (100%) rename seo-report.json => artifacts/reports/seo-report.json (100%) rename updated-json-ld.json => artifacts/reports/updated-json-ld.json (100%) create mode 100644 components/dropdown-page-shell.tsx create mode 100644 docs/README.md rename COOLIFY_LAUNCH.md => docs/operations/COOLIFY_LAUNCH.md (99%) rename LIGHTHOUSE_TESTING.md => docs/operations/LIGHTHOUSE_TESTING.md (99%) rename R2_SETUP.md => docs/operations/R2_SETUP.md (98%) rename SEO_SETUP.md => docs/operations/SEO_SETUP.md (99%) rename SHADCN_MCP_TROUBLESHOOTING.md => docs/operations/SHADCN_MCP_TROUBLESHOOTING.md (99%) rename SITE_CONFIGURATION.md => docs/operations/SITE_CONFIGURATION.md (99%) rename STAGING_DEPLOYMENT.md => docs/operations/STAGING_DEPLOYMENT.md (100%) rename .wordpress-removed.md => docs/reference/.wordpress-removed.md (99%) rename COMPONENT_AUDIT.md => docs/reference/COMPONENT_AUDIT.md (99%) rename FIXES_COMPLETED.md => docs/reference/FIXES_COMPLETED.md (100%) rename FIXES_PLAN.md => docs/reference/FIXES_PLAN.md (100%) rename STYLE_GUIDE.md => docs/reference/STYLE_GUIDE.md (97%) rename STYLING_STANDARDIZATION.md => docs/reference/STYLING_STANDARDIZATION.md (100%) create mode 100644 eslint.config.mjs rename modify_hero.py => scripts/one-off/modify_hero.py (100%) rename modify_home_page.py => scripts/one-off/modify_home_page.py (100%) rename modify_links.py => scripts/one-off/modify_links.py (100%) rename modify_parts_page.py => scripts/one-off/modify_parts_page.py (100%) rename modify_service_areas.py => scripts/one-off/modify_service_areas.py (100%) rename test-ebay-api.js => scripts/one-off/test-ebay-api.js (52%) rename test-fixes.sh => scripts/one-off/test-fixes.sh (100%) create mode 100644 tsconfig.typecheck.json diff --git a/.dockerignore b/.dockerignore index 38d699f9..350a49a1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -73,8 +73,11 @@ jspm_packages/ tmp/ temp/ .pnpm-store/ -.formatting-backups/ .cursor/ +.playwright-cli/ +output/ +docs/ +artifacts/ # Logs logs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..8c52ff93 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index 0742840b..622a33d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,10 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - # dependencies /node_modules # next.js /.next/ /out/ +/output/ # production /build @@ -15,10 +14,12 @@ npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* +/dev.log # env files .env* !.env.example +!.env.staging.example # vercel .vercel @@ -26,3 +27,10 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# local tooling caches +/.playwright-cli/ +/.pnpm-store/ + +# package manager drift +/package-lock.json diff --git a/.lighthouserc.js b/.lighthouserc.js index 4aeeced7..cf47e31f 100644 --- a/.lighthouserc.js +++ b/.lighthouserc.js @@ -2,45 +2,42 @@ module.exports = { ci: { collect: { - url: ['http://localhost:3000'], + url: ["http://localhost:3000"], numberOfRuns: 3, - startServerCommand: 'npm run start', - startServerReadyPattern: 'ready', + startServerCommand: "npm run start", + startServerReadyPattern: "ready", startServerReadyTimeout: 30000, }, assert: { assertions: { - 'categories:performance': ['error', { minScore: 1 }], - 'categories:accessibility': ['error', { minScore: 1 }], - 'categories:best-practices': ['error', { minScore: 1 }], - 'categories:seo': ['error', { minScore: 1 }], + "categories:performance": ["error", { minScore: 1 }], + "categories:accessibility": ["error", { minScore: 1 }], + "categories:best-practices": ["error", { minScore: 1 }], + "categories:seo": ["error", { minScore: 1 }], // Core Web Vitals - 'first-contentful-paint': ['error', { maxNumericValue: 1800 }], - 'largest-contentful-paint': ['error', { maxNumericValue: 2500 }], - 'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }], - 'total-blocking-time': ['error', { maxNumericValue: 200 }], - 'speed-index': ['error', { maxNumericValue: 3400 }], + "first-contentful-paint": ["error", { maxNumericValue: 1800 }], + "largest-contentful-paint": ["error", { maxNumericValue: 2500 }], + "cumulative-layout-shift": ["error", { maxNumericValue: 0.1 }], + "total-blocking-time": ["error", { maxNumericValue: 200 }], + "speed-index": ["error", { maxNumericValue: 3400 }], // Performance metrics - 'interactive': ['error', { maxNumericValue: 3800 }], - 'uses-optimized-images': 'error', - 'uses-text-compression': 'error', - 'uses-responsive-images': 'error', - 'modern-image-formats': 'error', - 'offscreen-images': 'error', - 'render-blocking-resources': 'error', - 'unused-css-rules': 'error', - 'unused-javascript': 'error', - 'efficient-animated-content': 'error', - 'preload-lcp-image': 'error', - 'uses-long-cache-ttl': 'error', - 'total-byte-weight': ['error', { maxNumericValue: 1600000 }], + interactive: ["error", { maxNumericValue: 3800 }], + "uses-optimized-images": "error", + "uses-text-compression": "error", + "uses-responsive-images": "error", + "modern-image-formats": "error", + "offscreen-images": "error", + "render-blocking-resources": "error", + "unused-css-rules": "error", + "unused-javascript": "error", + "efficient-animated-content": "error", + "preload-lcp-image": "error", + "uses-long-cache-ttl": "error", + "total-byte-weight": ["error", { maxNumericValue: 1600000 }], }, }, upload: { - target: 'temporary-public-storage', + target: "temporary-public-storage", }, }, -}; - - - +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..808ae6a3 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,15 @@ +.cursor +.next +.playwright-cli +.pnpm-store +artifacts +node_modules +out +output +pnpm-lock.yaml +public/json-ld +public/manual_inventory.json +public/manual_pages_full.json +public/manual_pages_parts.json +public/manual_pages_text.json +public/manual_parts_lookup.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..083ae08f --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "semi": false, + "singleQuote": false, + "trailingComma": "es5" +} diff --git a/app/about-us/page.tsx b/app/about-us/page.tsx index 54cd926e..c7d06156 100644 --- a/app/about-us/page.tsx +++ b/app/about-us/page.tsx @@ -1,6 +1,5 @@ import { notFound } from "next/navigation" -import { loadImageMapping } from "@/lib/wordpress-content" -import { generateSEOMetadata, generateStructuredData } from "@/lib/seo" +import { generateRegistryMetadata, generateRegistryStructuredData } from "@/lib/seo" import { getPageBySlug } from "@/lib/wordpress-data-loader" import { AboutPage } from "@/components/about-page" import type { Metadata } from "next" @@ -16,14 +15,10 @@ export async function generateMetadata(): Promise { } } - return generateSEOMetadata({ - title: page.title || "About Us", - description: page.seoDescription || page.excerpt || "", - excerpt: page.excerpt, + return generateRegistryMetadata("aboutUs", { date: page.date, modified: page.modified, image: page.images?.[0]?.localPath, - path: "/about-us", }) } @@ -35,28 +30,10 @@ export default async function AboutUsPage() { notFound() } - let structuredData - try { - structuredData = generateStructuredData({ - title: page.title || "About Us", - description: page.seoDescription || page.excerpt || "", - url: - page.link || - page.urlPath || - `https://rockymountainvending.com/about-us/`, - datePublished: page.date, - dateModified: page.modified || page.date, - type: "WebPage", - }) - } catch (e) { - structuredData = { - "@context": "https://schema.org", - "@type": "WebPage", - headline: page.title || "About Us", - description: page.seoDescription || "", - url: `https://rockymountainvending.com/about-us/`, - } - } + const structuredData = generateRegistryStructuredData("aboutUs", { + datePublished: page.date, + dateModified: page.modified || page.date, + }) return ( <> diff --git a/app/about/faqs/page.tsx b/app/about/faqs/page.tsx index 7789c04b..0778e470 100644 --- a/app/about/faqs/page.tsx +++ b/app/about/faqs/page.tsx @@ -1,83 +1,74 @@ -import { notFound } from 'next/navigation'; -import { loadImageMapping } from '@/lib/wordpress-content'; -import { generateSEOMetadata, generateStructuredData } from '@/lib/seo'; -import { getPageBySlug } from '@/lib/wordpress-data-loader'; -import { FAQSchema } from '@/components/faq-schema'; -import { FAQSection } from '@/components/faq-section'; -import type { Metadata } from 'next'; +import { notFound } from "next/navigation" +import { buildAbsoluteUrl } from "@/lib/seo-registry" +import { generateRegistryMetadata, generateRegistryStructuredData } from "@/lib/seo" +import { Breadcrumbs } from "@/components/breadcrumbs" +import { getPageBySlug } from "@/lib/wordpress-data-loader" +import { FAQSchema } from "@/components/faq-schema" +import { FAQSection } from "@/components/faq-section" +import type { Metadata } from "next" -const WORDPRESS_SLUG = 'faqs'; +const WORDPRESS_SLUG = "faqs" export async function generateMetadata(): Promise { - const page = getPageBySlug(WORDPRESS_SLUG); + const page = getPageBySlug(WORDPRESS_SLUG) if (!page) { return { - title: 'Page Not Found | Rocky Mountain Vending', - }; + title: "Page Not Found | Rocky Mountain Vending", + } } - return generateSEOMetadata({ - title: page.title || 'FAQs', - description: page.seoDescription || page.excerpt || '', - excerpt: page.excerpt, + return generateRegistryMetadata("faqs", { date: page.date, modified: page.modified, image: page.images?.[0]?.localPath, - }); + }) } export default async function FAQsPage() { try { - const page = getPageBySlug(WORDPRESS_SLUG); + const page = getPageBySlug(WORDPRESS_SLUG) if (!page) { - notFound(); + notFound() } // Extract FAQs from content - const faqs: Array<{ question: string; answer: string }> = []; + const faqs: Array<{ question: string; answer: string }> = [] if (page.content) { - const contentStr = String(page.content); + const contentStr = String(page.content) // Extract FAQ items from accordion structure - const questionMatches = contentStr.matchAll(/([^<]+)<\/span>/g); + const questionMatches = contentStr.matchAll( + /([^<]+)<\/span>/g + ) // Extract full answer content - const answerMatches = contentStr.matchAll(/
([\s\S]*?)<\/div>\s*<\/div>\s*/g); - - const questions = Array.from(questionMatches).map(m => m[1].trim()); - const answers = Array.from(answerMatches).map(m => { - let answer = m[1].trim(); - answer = answer.replace(/\n\s*\n/g, '\n').replace(/>\s+<').trim(); - return answer; - }); - + const answerMatches = contentStr.matchAll( + /
([\s\S]*?)<\/div>\s*<\/div>\s*/g + ) + + const questions = Array.from(questionMatches).map((m) => m[1].trim()) + const answers = Array.from(answerMatches).map((m) => { + let answer = m[1].trim() + answer = answer + .replace(/\n\s*\n/g, "\n") + .replace(/>\s+<") + .trim() + return answer + }) + // Match questions with answers questions.forEach((question, index) => { if (answers[index]) { - faqs.push({ question, answer: answers[index] }); + faqs.push({ question, answer: answers[index] }) } - }); + }) } - let structuredData; - try { - structuredData = generateStructuredData({ - title: page.title || 'FAQs', - description: page.seoDescription || page.excerpt || '', - url: page.link || page.urlPath || `https://rockymountainvending.com/about/faqs/`, - datePublished: page.date, - dateModified: page.modified || page.date, - type: 'WebPage', - }); - } catch (e) { - structuredData = { - '@context': 'https://schema.org', - '@type': 'WebPage', - headline: page.title || 'FAQs', - description: page.seoDescription || '', - url: `https://rockymountainvending.com/about/faqs/`, - }; - } + const pageUrl = buildAbsoluteUrl("/about/faqs") + const structuredData = generateRegistryStructuredData("faqs", { + datePublished: page.date, + dateModified: page.modified || page.date, + }) return ( <> @@ -86,28 +77,27 @@ export default async function FAQsPage() { dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} /> {faqs.length > 0 && ( - <> - + - - + + +
)} - ); + ) } catch (error) { - if (process.env.NODE_ENV === 'development') { - console.error('Error rendering FAQs page:', error); + if (process.env.NODE_ENV === "development") { + console.error("Error rendering FAQs page:", error) } - notFound(); + notFound() } } - - - - - - - - diff --git a/app/about/page.tsx b/app/about/page.tsx index 35d8d44e..b4b10a94 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -1,16 +1,11 @@ -import { generateSEOMetadata, generateStructuredData } from "@/lib/seo" +import { generateRegistryMetadata, generateRegistryStructuredData } from "@/lib/seo" import { AboutPage } from "@/components/about-page" import type { Metadata } from "next" import { businessConfig } from "@/lib/seo-config" export async function generateMetadata(): Promise { return { - ...generateSEOMetadata({ - title: "About Rocky Mountain Vending | Utah Vending Company", - description: - "Learn about Rocky Mountain Vending, the Utah service-area business behind our vending placement, repair, sales, and support services.", - path: "/about", - }), + ...generateRegistryMetadata("aboutLegacy"), alternates: { canonical: `${businessConfig.website}/about-us`, }, @@ -22,13 +17,7 @@ export async function generateMetadata(): Promise { } export default function About() { - const structuredData = generateStructuredData({ - title: "About Rocky Mountain Vending", - description: - "Learn about Rocky Mountain Vending, the Utah service-area business behind our vending placement, repair, sales, and support services.", - url: "https://rockymountainvending.com/about-us/", - type: "WebPage", - }) + const structuredData = generateRegistryStructuredData("aboutUs") return ( <> diff --git a/app/admin/calls/[id]/page.tsx b/app/admin/calls/[id]/page.tsx index 5626c6a0..bdbd88fb 100644 --- a/app/admin/calls/[id]/page.tsx +++ b/app/admin/calls/[id]/page.tsx @@ -1,31 +1,37 @@ -import Link from "next/link"; -import { notFound } from "next/navigation"; -import { fetchQuery } from "convex/nextjs"; -import { ArrowLeft, ExternalLink, Phone } from "lucide-react"; -import { api } from "@/convex/_generated/api"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import Link from "next/link" +import { notFound } from "next/navigation" +import { fetchQuery } from "convex/nextjs" +import { ArrowLeft, ExternalLink, Phone } from "lucide-react" +import { api } from "@/convex/_generated/api" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" import { formatPhoneCallDuration, formatPhoneCallTimestamp, normalizePhoneFromIdentity, -} from "@/lib/phone-calls"; +} from "@/lib/phone-calls" type PageProps = { params: Promise<{ - id: string; - }>; -}; + id: string + }> +} export default async function AdminCallDetailPage({ params }: PageProps) { - const { id } = await params; + const { id } = await params const detail = await fetchQuery(api.voiceSessions.getAdminPhoneCallDetail, { callId: id, - }); + }) if (!detail) { - notFound(); + notFound() } return ( @@ -33,13 +39,19 @@ export default async function AdminCallDetailPage({ params }: PageProps) {
- + Back to calls -

Phone Call Detail

+

+ Phone Call Detail +

- {normalizePhoneFromIdentity(detail.call.participantIdentity) || detail.call.participantIdentity} + {normalizePhoneFromIdentity(detail.call.participantIdentity) || + detail.call.participantIdentity}

@@ -51,58 +63,107 @@ export default async function AdminCallDetailPage({ params }: PageProps) { Call Status - Operational detail for this direct phone session. + + Operational detail for this direct phone session. +
-

Started

-

{formatPhoneCallTimestamp(detail.call.startedAt)}

+

+ Started +

+

+ {formatPhoneCallTimestamp(detail.call.startedAt)} +

-

Room

+

+ Room +

{detail.call.roomName}

-

Duration

-

{formatPhoneCallDuration(detail.call.durationMs)}

+

+ Duration +

+

+ {formatPhoneCallDuration(detail.call.durationMs)} +

-

Participant Identity

-

{detail.call.participantIdentity || "Unknown"}

+

+ Participant Identity +

+

+ {detail.call.participantIdentity || "Unknown"} +

-

Call Status

- +

+ Call Status +

+ {detail.call.callStatus}
-

Jessica Answered

-

{detail.call.answered ? "Yes" : "No"}

+

+ Jessica Answered +

+

+ {detail.call.answered ? "Yes" : "No"} +

-

Lead Outcome

+

+ Lead Outcome +

{detail.call.leadOutcome}

-

Email Summary

+

+ Email Summary +

{detail.call.notificationStatus}

-

Summary

-

{detail.call.summaryText || "No summary available yet."}

+

+ Summary +

+

+ {detail.call.summaryText || "No summary available yet."} +

-

Recording Status

-

{detail.call.recordingStatus || "Unavailable"}

+

+ Recording Status +

+

+ {detail.call.recordingStatus || "Unavailable"} +

-

Transcript Turns

+

+ Transcript Turns +

{detail.call.transcriptTurnCount}

{detail.call.recordingUrl ? (
- + Open recording @@ -110,8 +171,12 @@ export default async function AdminCallDetailPage({ params }: PageProps) { ) : null} {detail.call.notificationError ? (
-

Email Error

-

{detail.call.notificationError}

+

+ Email Error +

+

+ {detail.call.notificationError} +

) : null} @@ -121,37 +186,56 @@ export default async function AdminCallDetailPage({ params }: PageProps) { Linked Lead - {detail.linkedLead ? "Lead created from this phone call." : "No lead was created from this call."} + {detail.linkedLead + ? "Lead created from this phone call." + : "No lead was created from this call."} {detail.linkedLead ? ( <>
-

Contact

+

+ Contact +

{detail.linkedLead.firstName} {detail.linkedLead.lastName}

-

Lead Type

+

+ Lead Type +

{detail.linkedLead.type}

-

Email

-

{detail.linkedLead.email}

+

+ Email +

+

+ {detail.linkedLead.email} +

-

Phone

+

+ Phone +

{detail.linkedLead.phone}

-

Message

-

{detail.linkedLead.message || "—"}

+

+ Message +

+

+ {detail.linkedLead.message || "—"} +

) : ( -

Jessica handled the call, but it did not result in a submitted lead.

+

+ Jessica handled the call, but it did not result in a submitted + lead. +

)}
@@ -160,11 +244,15 @@ export default async function AdminCallDetailPage({ params }: PageProps) { Transcript - Complete mirrored transcript for this phone call. + + Complete mirrored transcript for this phone call. + {detail.turns.length === 0 ? ( -

No transcript turns were captured for this call.

+

+ No transcript turns were captured for this call. +

) : ( detail.turns.map((turn: any) => (
@@ -180,10 +268,11 @@ export default async function AdminCallDetailPage({ params }: PageProps) {
- ); + ) } export const metadata = { title: "Phone Call Detail | Admin", - description: "Review a mirrored direct phone call transcript and linked lead details", -}; + description: + "Review a mirrored direct phone call transcript and linked lead details", +} diff --git a/app/admin/calls/page.tsx b/app/admin/calls/page.tsx index 44da7c78..c7b89dc1 100644 --- a/app/admin/calls/page.tsx +++ b/app/admin/calls/page.tsx @@ -1,58 +1,67 @@ -import Link from "next/link"; -import { fetchQuery } from "convex/nextjs"; -import { Phone, Search } from "lucide-react"; -import { api } from "@/convex/_generated/api"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; +import Link from "next/link" +import { fetchQuery } from "convex/nextjs" +import { Phone, Search } from "lucide-react" +import { api } from "@/convex/_generated/api" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Input } from "@/components/ui/input" import { formatPhoneCallDuration, formatPhoneCallTimestamp, normalizePhoneFromIdentity, -} from "@/lib/phone-calls"; +} from "@/lib/phone-calls" type PageProps = { searchParams: Promise<{ - search?: string; - status?: "started" | "completed" | "failed"; - page?: string; - }>; -}; + search?: string + status?: "started" | "completed" | "failed" + page?: string + }> +} function getStatusVariant(status: "started" | "completed" | "failed") { if (status === "failed") { - return "destructive" as const; + return "destructive" as const } if (status === "started") { - return "secondary" as const; + return "secondary" as const } - return "default" as const; + return "default" as const } export default async function AdminCallsPage({ searchParams }: PageProps) { - const params = await searchParams; - const page = Math.max(1, Number.parseInt(params.page || "1", 10) || 1); - const status = params.status; - const search = params.search?.trim() || undefined; + const params = await searchParams + const page = Math.max(1, Number.parseInt(params.page || "1", 10) || 1) + const status = params.status + const search = params.search?.trim() || undefined const data = await fetchQuery(api.voiceSessions.listAdminPhoneCalls, { search, status, page, limit: 25, - }); + }) return (
-

Phone Calls

+

+ Phone Calls +

- Every direct LiveKit phone call mirrored into RMV admin, including partial and non-lead calls. + Every direct LiveKit phone call mirrored into RMV admin, including + partial and non-lead calls.

@@ -66,13 +75,20 @@ export default async function AdminCallsPage({ searchParams }: PageProps) { Call Inbox - Search by caller number, room, summary, or linked lead ID. + + Search by caller number, room, summary, or linked lead ID. +
- +