diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..e5c4a93 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,18 @@ +name: Claude Code Action +on: + issue_comment: + types: [created] + pull_request: + types: [opened, synchronize] + +jobs: + claude-response: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + steps: + - uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..75c4982 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,106 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Mexico Paradise Vacations — a travel certificate sales platform with payment plans, client portal, and admin dashboard. Built with Next.js 15 (App Router) + TypeScript + Tailwind CSS 4 + shadcn/ui. + +## Commands + +- **Dev server**: `npm run dev` (uses nodemon + tsx to run custom `server.ts`; HMR is disabled in favor of nodemon-based full reload) +- **Build**: `npm run build` +- **Production**: `npm run start` +- **Lint**: `npm run lint` +- **DB push schema**: `npm run db:push` +- **DB generate client**: `npm run db:generate` +- **DB migrate**: `npm run db:migrate` + +## Architecture + +### Custom Server (`server.ts`) +The app uses a custom Node HTTP server (not the default `next dev`/`next start`). It creates a Next.js app and attaches a Socket.IO server on `/api/socketio`. Dev mode runs via `nodemon --exec "npx tsx server.ts"`. + +### Dual Database Setup +- **Supabase** (`src/lib/supabase.ts`): Primary data store for the business domain — users, signups, payments, certificates. The Supabase client is initialized with hardcoded project URL/anon key. Database types are defined inline in this file. +- **Prisma + SQLite** (`src/lib/db.ts`, `prisma/schema.prisma`): Secondary database with a basic User/Post schema. Uses the singleton pattern to avoid multiple PrismaClient instances in dev. + +### Authentication +Simple demo auth in `src/lib/auth.ts` — password is hardcoded as `demo123`, user session stored in localStorage. Role-based: `admin` or `client`. + +### Payment Integration +`src/lib/maverick.ts` — `MaverickPaymentAPI` class wrapping the Maverick Payments REST API. Configured via `MAVERICK_API_URL` and `MAVERICK_API_KEY` env vars. + +### API Routes +All under `src/app/api/`: +- `POST /api/signup` — new vacation package signup +- `POST /api/payment/create` — create payment +- `POST /api/payment/confirm` — confirm payment status +- `POST /api/admin/refund` — process refund (admin only) +- `GET /api/health` — health check + +### Pages +- `/` — landing page +- `/client-portal` — client dashboard (auth-guarded) +- `/admin-portal` — admin dashboard (auth-guarded) +- `/payment/success`, `/payment/cancel` — payment result pages + +### UI Components +shadcn/ui (new-york style) in `src/components/ui/`. Custom components: `auth-guard.tsx`, `login-modal.tsx`. + +## Path Aliases + +`@/*` maps to `./src/*` (configured in tsconfig.json). + +## Environment Variables + +- `DATABASE_URL` — Prisma/SQLite connection string +- `NEXT_PUBLIC_APP_URL` — application URL +- `MAVERICK_API_URL` — Maverick Payments API base URL +- `MAVERICK_API_KEY` — Maverick Payments API key + +## Build Notes + +- TypeScript build errors are ignored (`typescript.ignoreBuildErrors: true` in next.config.ts) +- ESLint errors are ignored during builds (`eslint.ignoreDuringBuilds: true`) +- ESLint config is very permissive — most strict rules are turned off + +## Best Practices + +### Next.js App Router +- Use Server Components by default; only add `"use client"` when you need browser APIs, event handlers, or React hooks (useState, useEffect, etc.) +- Place data fetching in Server Components or API routes — never fetch in `useEffect` when a server-side approach works +- Use `route.ts` files for API routes; export named functions matching HTTP methods (`GET`, `POST`, `PUT`, `DELETE`) +- Return `NextResponse.json()` from API routes with appropriate status codes +- Use `loading.tsx`, `error.tsx`, and `not-found.tsx` for route-level UI states +- Use `layout.tsx` for shared UI that persists across navigations; avoid re-fetching data that a parent layout already provides +- Prefer Next.js `` over `` and `` over `` for optimized loading and client-side navigation +- Use `metadata` exports or `generateMetadata()` for SEO — not manual `` tags + +### Node.js / Server-Side +- Never block the event loop — use async/await for I/O, avoid synchronous file or network calls in request handlers +- Keep secrets in environment variables, never hardcode them (note: this project currently has hardcoded Supabase keys that should be moved to env vars) +- Use the Prisma singleton pattern from `src/lib/db.ts` to prevent connection exhaustion in dev +- Validate and sanitize all user input at API boundaries using Zod (already a dependency) +- Handle errors explicitly in API routes — return structured error responses, don't let unhandled exceptions leak stack traces +- Use `try/catch` around external API calls (Maverick, Supabase) and return meaningful error messages + +### React / Frontend +- Co-locate component state as close to where it's used as possible; lift state only when siblings need to share it +- Use Zustand (already installed) for global client state; avoid prop drilling more than 2 levels deep +- Use React Query (`@tanstack/react-query`, already installed) for server state — caching, refetching, and optimistic updates +- Prefer controlled form inputs with `react-hook-form` + Zod validation (both already installed) +- Use shadcn/ui components from `src/components/ui/` — don't rebuild existing primitives +- Add new shadcn components via `npx shadcn@latest add ` + +### TypeScript +- Use explicit return types on exported functions and API route handlers +- Define shared types/interfaces in dedicated files or alongside their domain (e.g., Supabase types in `src/lib/supabase.ts`) +- Prefer `interface` for object shapes that may be extended; use `type` for unions, intersections, and computed types +- Use Zod schemas as the single source of truth for validation and infer TypeScript types from them with `z.infer<>` + +### Styling +- Use Tailwind CSS utility classes; avoid custom CSS unless Tailwind cannot express the style +- Follow the shadcn/ui `new-york` style variant (configured in `components.json`) +- Use CSS variables defined in `src/app/globals.css` for theme colors +- Use `cn()` from `src/lib/utils.ts` to merge conditional Tailwind classes (combines `clsx` + `tailwind-merge`) diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs new file mode 100644 index 0000000..983b08d --- /dev/null +++ b/ecosystem.config.cjs @@ -0,0 +1,12 @@ +module.exports = { + apps: [{ + name: 'hi2b', + script: 'npx', + args: 'tsx server.ts', + cwd: '/opt/hi2b', + env: { + PORT: 3015, + NODE_ENV: 'production' + } + }] +} diff --git a/next.config.ts b/next.config.ts index ea90f9f..8d52773 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,11 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + images: { + remotePatterns: [ + { protocol: 'https', hostname: 'images.unsplash.com' }, + ], + }, typescript: { ignoreBuildErrors: true, }, diff --git a/package-lock.json b/package-lock.json index 9b632e3..ab9d4a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,19 +46,29 @@ "@tanstack/react-query": "^5.82.0", "@tanstack/react-table": "^8.21.3", "axios": "^1.10.0", + "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "framer-motion": "^12.23.2", + "gsap": "^3.14.2", "input-otp": "^1.4.2", + "jsonwebtoken": "^9.0.3", + "lenis": "^1.3.18", "lucide-react": "^0.525.0", + "mysql2": "^3.20.0", "next": "15.3.5", "next-auth": "^4.24.11", "next-intl": "^4.3.4", "next-themes": "^0.4.6", + "nodemailer": "^6.10.1", + "pdfkit": "^0.18.0", "prisma": "^6.11.1", + "puppeteer-core": "^24.39.1", + "puppeteer-extra": "^3.3.6", + "puppeteer-extra-plugin-stealth": "^2.11.2", "react": "^19.0.0", "react-day-picker": "^9.8.0", "react-dom": "^19.0.0", @@ -67,6 +77,7 @@ "react-resizable-panels": "^3.0.3", "react-syntax-highlighter": "^15.6.1", "recharts": "^2.15.4", + "resend": "^6.9.4", "sharp": "^0.34.3", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", @@ -83,7 +94,10 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^20", + "@types/nodemailer": "^7.0.11", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", @@ -2595,6 +2609,30 @@ "node": ">= 10" } }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2737,6 +2775,27 @@ "@prisma/debug": "6.17.1" } }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@radix-ui/colors": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", @@ -4206,6 +4265,12 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", @@ -4693,6 +4758,12 @@ "node": ">=18" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -4710,6 +4781,13 @@ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "license": "MIT" }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/cors": { "version": "2.8.19", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", @@ -4829,6 +4907,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -4853,6 +4942,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/nodemailer": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz", + "integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/phoenix": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", @@ -4893,6 +4992,16 @@ "@types/node": "*" } }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", @@ -5499,6 +5608,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -5535,7 +5653,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -5588,6 +5705,15 @@ "dequal": "^2.0.3" } }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -5748,6 +5874,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -5787,6 +5925,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/axe-core": { "version": "4.11.0", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", @@ -5818,6 +5965,20 @@ "node": ">= 0.4" } }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -5832,9 +5993,95 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz", + "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz", + "integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.1.tgz", + "integrity": "sha512-bSeR8RfvbRwDpD7HWZvn8M3uYNDrk7m9DQjYOFkENZlXW8Ju/MPaqUPQq5LqJ3kyjEm07siTaAQ7wBKCU59oHg==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.21.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -5864,6 +6111,24 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/basic-ftp": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -5881,7 +6146,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -5901,6 +6165,15 @@ "node": ">=8" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -5925,6 +6198,21 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -6186,6 +6474,28 @@ "node": ">=18" } }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/citty": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", @@ -6225,6 +6535,45 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", + "integrity": "sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==", + "license": "MIT", + "dependencies": { + "for-own": "^0.1.3", + "is-plain-object": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "shallow-clone": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -6281,7 +6630,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -6294,7 +6642,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -6329,7 +6676,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/confbox": { @@ -6537,6 +6883,15 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -6656,6 +7011,15 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/deepmerge-ts": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", @@ -6707,6 +7071,20 @@ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "license": "MIT" }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -6716,6 +7094,15 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -6759,6 +7146,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1581282", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", + "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", + "license": "BSD-3-Clause" + }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", @@ -6839,6 +7238,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/effect": { "version": "3.16.12", "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz", @@ -6893,6 +7301,15 @@ "node": ">=14" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/engine.io": { "version": "6.6.4", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", @@ -7237,6 +7654,15 @@ "@esbuild/win32-x64": "0.25.10" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-carriage": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.3.1.tgz", @@ -7256,6 +7682,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "9.37.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", @@ -7661,6 +8108,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -7691,7 +8151,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -7725,7 +8184,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -7747,6 +8205,15 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/exsolve": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", @@ -7768,6 +8235,26 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", @@ -7794,7 +8281,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-equals": { @@ -7806,6 +8292,12 @@ "node": ">=6.0.0" } }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -7850,6 +8342,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -7873,6 +8371,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -7957,6 +8464,23 @@ } } }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -7973,6 +8497,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==", + "license": "MIT", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -8024,6 +8569,26 @@ } } }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -8078,6 +8643,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -8088,6 +8662,15 @@ "node": ">= 0.4" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -8134,6 +8717,21 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -8164,6 +8762,20 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/giget": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", @@ -8181,6 +8793,27 @@ "giget": "dist/cli.mjs" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -8240,7 +8873,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -8250,6 +8882,12 @@ "dev": true, "license": "MIT" }, + "node_modules/gsap": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz", + "integrity": "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -8481,6 +9119,48 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -8545,6 +9225,23 @@ "node": ">=0.8.19" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/inline-style-parser": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", @@ -8603,6 +9300,15 @@ "tslib": "^2.8.0" } }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -8711,6 +9417,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, "node_modules/is-bun-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", @@ -8795,6 +9507,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -8821,6 +9542,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -8929,6 +9659,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -9088,6 +9836,15 @@ "dev": true, "license": "ISC" }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isomorphic.js": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", @@ -9144,6 +9901,12 @@ "node": ">=14" } }, + "node_modules/js-md5": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz", + "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9196,6 +9959,40 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -9212,6 +10009,27 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -9222,6 +10040,18 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -9251,6 +10081,41 @@ "node": ">=0.10" } }, + "node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lenis": { + "version": "1.3.18", + "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.18.tgz", + "integrity": "sha512-7KBl3V7vx5y1h05pu9fNFZS66I0+1eZ+zUGNNNBKtEn3BONZy+nkHWvdEe2b+zKT+6WX1x7zyOb1zbYYOs6tcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/darkroomengineering" + }, + "peerDependencies": { + "@nuxt/kit": ">=3.0.0", + "react": ">=17.0.0", + "vue": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "react": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -9532,6 +10397,25 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -9560,6 +10444,42 @@ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9567,6 +10487,18 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -9628,6 +10560,21 @@ "node": ">=10" } }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/lucide-react": { "version": "0.525.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", @@ -9953,6 +10900,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/merge-deep": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz", + "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "clone-deep": "^0.2.4", + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -10708,7 +11669,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -10750,6 +11710,34 @@ "node": ">= 18" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==", + "license": "MIT", + "dependencies": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-object/node_modules/for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/motion-dom": { "version": "12.23.23", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", @@ -10780,6 +11768,40 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mysql2": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.20.0.tgz", + "integrity": "sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -10830,6 +11852,15 @@ "node": ">= 0.6" } }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/next": { "version": "15.3.5", "resolved": "https://registry.npmjs.org/next/-/next-15.3.5.tgz", @@ -11002,6 +12033,15 @@ "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", "license": "MIT" }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", @@ -11235,6 +12275,15 @@ "node": "^10.13.0 || >=12.0.0" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/openid-client": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", @@ -11324,6 +12373,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -11372,6 +12459,15 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -11395,6 +12491,26 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pdfkit": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.18.0.tgz", + "integrity": "sha512-NvUwSDZ0eYEzqAiWwVQkRkjYUkZ48kcsHuCO31ykqPPIVkwoSDjDGiwIgHHNtsiwls3z3P/zy4q00hl2chg2Ug==", + "license": "MIT", + "dependencies": { + "@noble/ciphers": "^1.0.0", + "@noble/hashes": "^1.6.0", + "fontkit": "^2.0.4", + "js-md5": "^0.8.3", + "linebreak": "^1.1.0", + "png-js": "^1.0.0" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -11431,6 +12547,11 @@ "pathe": "^2.0.3" } }, + "node_modules/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -11441,6 +12562,12 @@ "node": ">= 0.4" } }, + "node_modules/postal-mime": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.3.tgz", + "integrity": "sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==", + "license": "MIT-0" + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -11568,6 +12695,15 @@ "node": ">=6" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -11595,27 +12731,240 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, - "license": "MIT" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer-core": { + "version": "24.39.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.39.1.tgz", + "integrity": "sha512-AMqQIKoEhPS6CilDzw0Gd1brLri3emkC+1N2J6ZCCuY1Cglo56M63S0jOeBZDQlemOiRd686MYVMl9ELJBzN3A==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1581282", + "typed-query-selector": "^2.12.1", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/puppeteer-extra": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/puppeteer-extra/-/puppeteer-extra-3.3.6.tgz", + "integrity": "sha512-rsLBE/6mMxAjlLd06LuGacrukP2bqbzKCLzV1vrhHFavqQE/taQ2UXv3H5P0Ls7nsrASa+6x3bDbXHpqMwq+7A==", + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.0", + "debug": "^4.1.1", + "deepmerge": "^4.2.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "@types/puppeteer": "*", + "puppeteer": "*", + "puppeteer-core": "*" + }, + "peerDependenciesMeta": { + "@types/puppeteer": { + "optional": true + }, + "puppeteer": { + "optional": true + }, + "puppeteer-core": { + "optional": true + } + } + }, + "node_modules/puppeteer-extra-plugin": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin/-/puppeteer-extra-plugin-3.2.3.tgz", + "integrity": "sha512-6RNy0e6pH8vaS3akPIKGg28xcryKscczt4wIl0ePciZENGE2yoaQJNd17UiEbdmh5/6WW6dPcfRWT9lxBwCi2Q==", + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.0", + "debug": "^4.1.1", + "merge-deep": "^3.0.1" + }, + "engines": { + "node": ">=9.11.2" + }, + "peerDependencies": { + "playwright-extra": "*", + "puppeteer-extra": "*" + }, + "peerDependenciesMeta": { + "playwright-extra": { + "optional": true + }, + "puppeteer-extra": { + "optional": true + } + } + }, + "node_modules/puppeteer-extra-plugin-stealth": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin-stealth/-/puppeteer-extra-plugin-stealth-2.11.2.tgz", + "integrity": "sha512-bUemM5XmTj9i2ZerBzsk2AN5is0wHMNE6K0hXBzBXOzP5m5G3Wl0RHhiqKeHToe/uIH8AoZiGhc1tCkLZQPKTQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "puppeteer-extra-plugin": "^3.2.3", + "puppeteer-extra-plugin-user-preferences": "^2.4.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "playwright-extra": "*", + "puppeteer-extra": "*" + }, + "peerDependenciesMeta": { + "playwright-extra": { + "optional": true + }, + "puppeteer-extra": { + "optional": true + } + } + }, + "node_modules/puppeteer-extra-plugin-user-data-dir": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin-user-data-dir/-/puppeteer-extra-plugin-user-data-dir-2.4.1.tgz", + "integrity": "sha512-kH1GnCcqEDoBXO7epAse4TBPJh9tEpVEK/vkedKfjOVOhZAvLkHGc9swMs5ChrJbRnf8Hdpug6TJlEuimXNQ+g==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^10.0.0", + "puppeteer-extra-plugin": "^3.2.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "playwright-extra": "*", + "puppeteer-extra": "*" + }, + "peerDependenciesMeta": { + "playwright-extra": { + "optional": true + }, + "puppeteer-extra": { + "optional": true + } + } + }, + "node_modules/puppeteer-extra-plugin-user-preferences": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin-user-preferences/-/puppeteer-extra-plugin-user-preferences-2.4.1.tgz", + "integrity": "sha512-i1oAZxRbc1bk8MZufKCruCEC3CCafO9RKMkkodZltI4OqibLFXF3tj6HZ4LZ9C5vCXZjYcDWazgtY69mnmrQ9A==", "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "deepmerge": "^4.2.2", + "puppeteer-extra-plugin": "^3.2.3", + "puppeteer-extra-plugin-user-data-dir": "^2.4.1" + }, "engines": { - "node": ">=6" + "node": ">=8" + }, + "peerDependencies": { + "playwright-extra": "*", + "puppeteer-extra": "*" + }, + "peerDependenciesMeta": { + "playwright-extra": { + "optional": true + }, + "puppeteer-extra": { + "optional": true + } } }, "node_modules/pure-rand": { @@ -12152,6 +13501,36 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resend": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.9.4.tgz", + "integrity": "sha512-/M3dsJzu5OgozqVsA4Psd/1L7EdePgOIIxClas453GOQYFG3VHc2ZyCHZFlvqsc9aZCCd2BJRRqZgWC8D9c7/g==", + "license": "MIT", + "dependencies": { + "postal-mime": "2.7.3", + "svix": "1.86.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@react-email/render": "*" + }, + "peerDependenciesMeta": { + "@react-email/render": { + "optional": true + } + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -12192,6 +13571,12 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -12203,6 +13588,22 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12259,6 +13660,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -12294,6 +13715,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -12313,9 +13740,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -12373,6 +13800,42 @@ "node": ">= 0.4" } }, + "node_modules/shallow-clone": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", + "integrity": "sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "mixin-object": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/kind-of": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sharp": { "version": "0.34.4", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", @@ -12527,6 +13990,16 @@ "node": ">=10" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/socket.io": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", @@ -12651,6 +14124,34 @@ } } }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -12661,6 +14162,16 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -12680,6 +14191,21 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -12687,6 +14213,16 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/static-browser-server": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/static-browser-server/-/static-browser-server-1.0.3.tgz", @@ -12721,12 +14257,43 @@ "node": ">=10.0.0" } }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/strict-event-emitter": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.4.6.tgz", "integrity": "sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==", "license": "MIT" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -12854,6 +14421,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -12950,6 +14529,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svix": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.86.0.tgz", + "integrity": "sha512-/HTvXwjLJe1l/MsLXAO1ddCYxElJk4eNR4DzOjDOEmGrPN/3BtBE8perGwMAaJ2sT5T172VkBYzmHcjUfM1JRQ==", + "license": "MIT", + "dependencies": { + "standardwebhooks": "1.0.0", + "uuid": "^10.0.0" + } + }, + "node_modules/svix/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", @@ -13012,6 +14614,32 @@ "node": ">=18" } }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -13022,6 +14650,30 @@ "node": ">=18" } }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -13289,6 +14941,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-query-selector": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", + "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -13335,6 +14993,26 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, "node_modules/unidiff": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unidiff/-/unidiff-1.0.4.tgz", @@ -13444,6 +15122,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unrs-resolver": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", @@ -13664,6 +15351,12 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -13795,6 +15488,29 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", @@ -13833,12 +15549,58 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yjs": { "version": "13.6.27", "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz", diff --git a/package.json b/package.json index 11d18da..208d3dd 100644 --- a/package.json +++ b/package.json @@ -51,19 +51,29 @@ "@tanstack/react-query": "^5.82.0", "@tanstack/react-table": "^8.21.3", "axios": "^1.10.0", + "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "framer-motion": "^12.23.2", + "gsap": "^3.14.2", "input-otp": "^1.4.2", + "jsonwebtoken": "^9.0.3", + "lenis": "^1.3.18", "lucide-react": "^0.525.0", + "mysql2": "^3.20.0", "next": "15.3.5", "next-auth": "^4.24.11", "next-intl": "^4.3.4", "next-themes": "^0.4.6", + "nodemailer": "^6.10.1", + "pdfkit": "^0.18.0", "prisma": "^6.11.1", + "puppeteer-core": "^24.39.1", + "puppeteer-extra": "^3.3.6", + "puppeteer-extra-plugin-stealth": "^2.11.2", "react": "^19.0.0", "react-day-picker": "^9.8.0", "react-dom": "^19.0.0", @@ -72,6 +82,7 @@ "react-resizable-panels": "^3.0.3", "react-syntax-highlighter": "^15.6.1", "recharts": "^2.15.4", + "resend": "^6.9.4", "sharp": "^0.34.3", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", @@ -88,7 +99,10 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^20", + "@types/nodemailer": "^7.0.11", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", diff --git a/public/ebooks/budget-luxury-travel.pdf b/public/ebooks/budget-luxury-travel.pdf new file mode 100644 index 0000000..7142574 Binary files /dev/null and b/public/ebooks/budget-luxury-travel.pdf differ diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..8c73bab --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/images/cdn/photo-1414235077428-338989a2e8c0.jpg b/public/images/cdn/photo-1414235077428-338989a2e8c0.jpg new file mode 100644 index 0000000..0bb96d8 Binary files /dev/null and b/public/images/cdn/photo-1414235077428-338989a2e8c0.jpg differ diff --git a/public/images/cdn/photo-1436491865332-7a61a109db05.jpg b/public/images/cdn/photo-1436491865332-7a61a109db05.jpg new file mode 100644 index 0000000..a9a1cfe Binary files /dev/null and b/public/images/cdn/photo-1436491865332-7a61a109db05.jpg differ diff --git a/public/images/cdn/photo-1438761681033-6461ffad8d80.jpg b/public/images/cdn/photo-1438761681033-6461ffad8d80.jpg new file mode 100644 index 0000000..4973133 Binary files /dev/null and b/public/images/cdn/photo-1438761681033-6461ffad8d80.jpg differ diff --git a/public/images/cdn/photo-1468413253725-0d5181091f76.jpg b/public/images/cdn/photo-1468413253725-0d5181091f76.jpg new file mode 100644 index 0000000..da6acd5 Binary files /dev/null and b/public/images/cdn/photo-1468413253725-0d5181091f76.jpg differ diff --git a/public/images/cdn/photo-1472099645785-5658abf4ff4e.jpg b/public/images/cdn/photo-1472099645785-5658abf4ff4e.jpg new file mode 100644 index 0000000..4e30621 Binary files /dev/null and b/public/images/cdn/photo-1472099645785-5658abf4ff4e.jpg differ diff --git a/public/images/cdn/photo-1473116763249-2faaef81ccda.jpg b/public/images/cdn/photo-1473116763249-2faaef81ccda.jpg new file mode 100644 index 0000000..921fb6b Binary files /dev/null and b/public/images/cdn/photo-1473116763249-2faaef81ccda.jpg differ diff --git a/public/images/cdn/photo-1494790108377-be9c29b29330.jpg b/public/images/cdn/photo-1494790108377-be9c29b29330.jpg new file mode 100644 index 0000000..35b867c Binary files /dev/null and b/public/images/cdn/photo-1494790108377-be9c29b29330.jpg differ diff --git a/public/images/cdn/photo-1497215728101-856f4ea42174.jpg b/public/images/cdn/photo-1497215728101-856f4ea42174.jpg new file mode 100644 index 0000000..6af2cf5 Binary files /dev/null and b/public/images/cdn/photo-1497215728101-856f4ea42174.jpg differ diff --git a/public/images/cdn/photo-1500648767791-00dcc994a43e.jpg b/public/images/cdn/photo-1500648767791-00dcc994a43e.jpg new file mode 100644 index 0000000..8e4ed67 Binary files /dev/null and b/public/images/cdn/photo-1500648767791-00dcc994a43e.jpg differ diff --git a/public/images/cdn/photo-1504674900247-0877df9cc836.jpg b/public/images/cdn/photo-1504674900247-0877df9cc836.jpg new file mode 100644 index 0000000..5e65cb2 Binary files /dev/null and b/public/images/cdn/photo-1504674900247-0877df9cc836.jpg differ diff --git a/public/images/cdn/photo-1506794778202-cad84cf45f1d.jpg b/public/images/cdn/photo-1506794778202-cad84cf45f1d.jpg new file mode 100644 index 0000000..c0aad52 Binary files /dev/null and b/public/images/cdn/photo-1506794778202-cad84cf45f1d.jpg differ diff --git a/public/images/cdn/photo-1506929562872-bb421503ef21.jpg b/public/images/cdn/photo-1506929562872-bb421503ef21.jpg new file mode 100644 index 0000000..f78a7f0 Binary files /dev/null and b/public/images/cdn/photo-1506929562872-bb421503ef21.jpg differ diff --git a/public/images/cdn/photo-1507525428034-b723cf961d3e.jpg b/public/images/cdn/photo-1507525428034-b723cf961d3e.jpg new file mode 100644 index 0000000..da6acd5 Binary files /dev/null and b/public/images/cdn/photo-1507525428034-b723cf961d3e.jpg differ diff --git a/public/images/cdn/photo-1510097467424-192c0d76b5b6.jpg b/public/images/cdn/photo-1510097467424-192c0d76b5b6.jpg new file mode 100644 index 0000000..60a3e11 Binary files /dev/null and b/public/images/cdn/photo-1510097467424-192c0d76b5b6.jpg differ diff --git a/public/images/cdn/photo-1510097467424-192d713fd8b2.jpg b/public/images/cdn/photo-1510097467424-192d713fd8b2.jpg new file mode 100644 index 0000000..60a3e11 Binary files /dev/null and b/public/images/cdn/photo-1510097467424-192d713fd8b2.jpg differ diff --git a/public/images/cdn/photo-1512100356356-de1b84283e18.jpg b/public/images/cdn/photo-1512100356356-de1b84283e18.jpg new file mode 100644 index 0000000..f072663 Binary files /dev/null and b/public/images/cdn/photo-1512100356356-de1b84283e18.jpg differ diff --git a/public/images/cdn/photo-1514362545857-3bc16c4c7d1b.jpg b/public/images/cdn/photo-1514362545857-3bc16c4c7d1b.jpg new file mode 100644 index 0000000..406d37b Binary files /dev/null and b/public/images/cdn/photo-1514362545857-3bc16c4c7d1b.jpg differ diff --git a/public/images/cdn/photo-1517248135467-4c7edcad34c4.jpg b/public/images/cdn/photo-1517248135467-4c7edcad34c4.jpg new file mode 100644 index 0000000..0c2558f Binary files /dev/null and b/public/images/cdn/photo-1517248135467-4c7edcad34c4.jpg differ diff --git a/public/images/cdn/photo-1518105779142-d975f22f1b0a.jpg b/public/images/cdn/photo-1518105779142-d975f22f1b0a.jpg new file mode 100644 index 0000000..1efb6bb Binary files /dev/null and b/public/images/cdn/photo-1518105779142-d975f22f1b0a.jpg differ diff --git a/public/images/cdn/photo-1518638150340-f706e86654de.jpg b/public/images/cdn/photo-1518638150340-f706e86654de.jpg new file mode 100644 index 0000000..3b99318 Binary files /dev/null and b/public/images/cdn/photo-1518638150340-f706e86654de.jpg differ diff --git a/public/images/cdn/photo-1519046904884-53103b34b206.jpg b/public/images/cdn/photo-1519046904884-53103b34b206.jpg new file mode 100644 index 0000000..a9a1cfe Binary files /dev/null and b/public/images/cdn/photo-1519046904884-53103b34b206.jpg differ diff --git a/public/images/cdn/photo-1520250497591-112f2f40a3f4.jpg b/public/images/cdn/photo-1520250497591-112f2f40a3f4.jpg new file mode 100644 index 0000000..b65704d Binary files /dev/null and b/public/images/cdn/photo-1520250497591-112f2f40a3f4.jpg differ diff --git a/public/images/cdn/photo-1520454974749-611b7248ffdb.jpg b/public/images/cdn/photo-1520454974749-611b7248ffdb.jpg new file mode 100644 index 0000000..46538d5 Binary files /dev/null and b/public/images/cdn/photo-1520454974749-611b7248ffdb.jpg differ diff --git a/public/images/cdn/photo-1522529599102-193c0d76b5b6.jpg b/public/images/cdn/photo-1522529599102-193c0d76b5b6.jpg new file mode 100644 index 0000000..2e9c327 Binary files /dev/null and b/public/images/cdn/photo-1522529599102-193c0d76b5b6.jpg differ diff --git a/public/images/cdn/photo-1527734055665-8def83921139.jpg b/public/images/cdn/photo-1527734055665-8def83921139.jpg new file mode 100644 index 0000000..fd060fc Binary files /dev/null and b/public/images/cdn/photo-1527734055665-8def83921139.jpg differ diff --git a/public/images/cdn/photo-1530866495561-507c83010e82.jpg b/public/images/cdn/photo-1530866495561-507c83010e82.jpg new file mode 100644 index 0000000..3f09004 Binary files /dev/null and b/public/images/cdn/photo-1530866495561-507c83010e82.jpg differ diff --git a/public/images/cdn/photo-1540497077202-7c8a3999166f.jpg b/public/images/cdn/photo-1540497077202-7c8a3999166f.jpg new file mode 100644 index 0000000..b8a6637 Binary files /dev/null and b/public/images/cdn/photo-1540497077202-7c8a3999166f.jpg differ diff --git a/public/images/cdn/photo-1540541338287-41700207dee6.jpg b/public/images/cdn/photo-1540541338287-41700207dee6.jpg new file mode 100644 index 0000000..552163a Binary files /dev/null and b/public/images/cdn/photo-1540541338287-41700207dee6.jpg differ diff --git a/public/images/cdn/photo-1544005313-94ddf0286df2.jpg b/public/images/cdn/photo-1544005313-94ddf0286df2.jpg new file mode 100644 index 0000000..393c3a0 Binary files /dev/null and b/public/images/cdn/photo-1544005313-94ddf0286df2.jpg differ diff --git a/public/images/cdn/photo-1544161515-4ab6ce6db874.jpg b/public/images/cdn/photo-1544161515-4ab6ce6db874.jpg new file mode 100644 index 0000000..9bda222 Binary files /dev/null and b/public/images/cdn/photo-1544161515-4ab6ce6db874.jpg differ diff --git a/public/images/cdn/photo-1544551763-46a013bb70d5.jpg b/public/images/cdn/photo-1544551763-46a013bb70d5.jpg new file mode 100644 index 0000000..8b5645c Binary files /dev/null and b/public/images/cdn/photo-1544551763-46a013bb70d5.jpg differ diff --git a/public/images/cdn/photo-1550966871-3ed3cdb51f3a.jpg b/public/images/cdn/photo-1550966871-3ed3cdb51f3a.jpg new file mode 100644 index 0000000..170ad64 Binary files /dev/null and b/public/images/cdn/photo-1550966871-3ed3cdb51f3a.jpg differ diff --git a/public/images/cdn/photo-1551882547-ff40c63fe5fa.jpg b/public/images/cdn/photo-1551882547-ff40c63fe5fa.jpg new file mode 100644 index 0000000..a68acb2 Binary files /dev/null and b/public/images/cdn/photo-1551882547-ff40c63fe5fa.jpg differ diff --git a/public/images/cdn/photo-1552074284-5e88ef1aef18.jpg b/public/images/cdn/photo-1552074284-5e88ef1aef18.jpg new file mode 100644 index 0000000..fe98f56 Binary files /dev/null and b/public/images/cdn/photo-1552074284-5e88ef1aef18.jpg differ diff --git a/public/images/cdn/photo-1555396273-367ea4eb4db5.jpg b/public/images/cdn/photo-1555396273-367ea4eb4db5.jpg new file mode 100644 index 0000000..b4e3e49 Binary files /dev/null and b/public/images/cdn/photo-1555396273-367ea4eb4db5.jpg differ diff --git a/public/images/cdn/photo-1558618666-fcd25c85f82e.jpg b/public/images/cdn/photo-1558618666-fcd25c85f82e.jpg new file mode 100644 index 0000000..fd060fc Binary files /dev/null and b/public/images/cdn/photo-1558618666-fcd25c85f82e.jpg differ diff --git a/public/images/cdn/photo-1566073771259-6a8506099945.jpg b/public/images/cdn/photo-1566073771259-6a8506099945.jpg new file mode 100644 index 0000000..cd95948 Binary files /dev/null and b/public/images/cdn/photo-1566073771259-6a8506099945.jpg differ diff --git a/public/images/cdn/photo-1571896349842-33c89424de2d.jpg b/public/images/cdn/photo-1571896349842-33c89424de2d.jpg new file mode 100644 index 0000000..c3bc39e Binary files /dev/null and b/public/images/cdn/photo-1571896349842-33c89424de2d.jpg differ diff --git a/public/images/cdn/photo-1575762568427-4b23bf947729.jpg b/public/images/cdn/photo-1575762568427-4b23bf947729.jpg new file mode 100644 index 0000000..90a46fa Binary files /dev/null and b/public/images/cdn/photo-1575762568427-4b23bf947729.jpg differ diff --git a/public/images/cdn/photo-1576610616656-d3aa5d1f4534.jpg b/public/images/cdn/photo-1576610616656-d3aa5d1f4534.jpg new file mode 100644 index 0000000..145bb13 Binary files /dev/null and b/public/images/cdn/photo-1576610616656-d3aa5d1f4534.jpg differ diff --git a/public/images/cdn/photo-1580415200778-625cb1890ab5.jpg b/public/images/cdn/photo-1580415200778-625cb1890ab5.jpg new file mode 100644 index 0000000..f31172b Binary files /dev/null and b/public/images/cdn/photo-1580415200778-625cb1890ab5.jpg differ diff --git a/public/images/cdn/photo-1580846629083-02669741360a.jpg b/public/images/cdn/photo-1580846629083-02669741360a.jpg new file mode 100644 index 0000000..ec12d0a Binary files /dev/null and b/public/images/cdn/photo-1580846629083-02669741360a.jpg differ diff --git a/public/images/cdn/photo-1581710862235-eb6e05d8783f.jpg b/public/images/cdn/photo-1581710862235-eb6e05d8783f.jpg new file mode 100644 index 0000000..3f09004 Binary files /dev/null and b/public/images/cdn/photo-1581710862235-eb6e05d8783f.jpg differ diff --git a/public/images/cdn/photo-1582719508461-905c673771fd.jpg b/public/images/cdn/photo-1582719508461-905c673771fd.jpg new file mode 100644 index 0000000..6d25b08 Binary files /dev/null and b/public/images/cdn/photo-1582719508461-905c673771fd.jpg differ diff --git a/public/images/cdn/photo-1584132967334-10e028bd69f7.jpg b/public/images/cdn/photo-1584132967334-10e028bd69f7.jpg new file mode 100644 index 0000000..19dafca Binary files /dev/null and b/public/images/cdn/photo-1584132967334-10e028bd69f7.jpg differ diff --git a/public/images/cdn/photo-1585793753011-397e6e4668d6.jpg b/public/images/cdn/photo-1585793753011-397e6e4668d6.jpg new file mode 100644 index 0000000..7cff1d7 Binary files /dev/null and b/public/images/cdn/photo-1585793753011-397e6e4668d6.jpg differ diff --git a/public/images/cdn/photo-1593655600619-a88c11180241.jpg b/public/images/cdn/photo-1593655600619-a88c11180241.jpg new file mode 100644 index 0000000..f31172b Binary files /dev/null and b/public/images/cdn/photo-1593655600619-a88c11180241.jpg differ diff --git a/public/images/cdn/photo-1596436889106-be35e843f974.jpg b/public/images/cdn/photo-1596436889106-be35e843f974.jpg new file mode 100644 index 0000000..6430c7a Binary files /dev/null and b/public/images/cdn/photo-1596436889106-be35e843f974.jpg differ diff --git a/public/images/cdn/photo-1602002418816-5c0aeef426aa.jpg b/public/images/cdn/photo-1602002418816-5c0aeef426aa.jpg new file mode 100644 index 0000000..a4a9724 Binary files /dev/null and b/public/images/cdn/photo-1602002418816-5c0aeef426aa.jpg differ diff --git a/public/images/cdn/photo-1615460549969-36fa19521a4f.jpg b/public/images/cdn/photo-1615460549969-36fa19521a4f.jpg new file mode 100644 index 0000000..c3bd54c Binary files /dev/null and b/public/images/cdn/photo-1615460549969-36fa19521a4f.jpg differ diff --git a/public/images/destinations/cabo-2.jpg b/public/images/destinations/cabo-2.jpg new file mode 100644 index 0000000..ec12d0a Binary files /dev/null and b/public/images/destinations/cabo-2.jpg differ diff --git a/public/images/destinations/cabo-3.jpg b/public/images/destinations/cabo-3.jpg new file mode 100644 index 0000000..fd060fc Binary files /dev/null and b/public/images/destinations/cabo-3.jpg differ diff --git a/public/images/destinations/cabo.jpg b/public/images/destinations/cabo.jpg new file mode 100644 index 0000000..f31172b Binary files /dev/null and b/public/images/destinations/cabo.jpg differ diff --git a/public/images/destinations/cancun-2.jpg b/public/images/destinations/cancun-2.jpg new file mode 100644 index 0000000..fe98f56 Binary files /dev/null and b/public/images/destinations/cancun-2.jpg differ diff --git a/public/images/destinations/cancun.jpg b/public/images/destinations/cancun.jpg new file mode 100644 index 0000000..60a3e11 Binary files /dev/null and b/public/images/destinations/cancun.jpg differ diff --git a/public/images/destinations/puerto-vallarta-2.jpg b/public/images/destinations/puerto-vallarta-2.jpg new file mode 100644 index 0000000..90a46fa Binary files /dev/null and b/public/images/destinations/puerto-vallarta-2.jpg differ diff --git a/public/images/destinations/puerto-vallarta.jpg b/public/images/destinations/puerto-vallarta.jpg new file mode 100644 index 0000000..7cff1d7 Binary files /dev/null and b/public/images/destinations/puerto-vallarta.jpg differ diff --git a/public/images/destinations/riviera-maya.jpg b/public/images/destinations/riviera-maya.jpg new file mode 100644 index 0000000..3f09004 Binary files /dev/null and b/public/images/destinations/riviera-maya.jpg differ diff --git a/public/images/hero/beach-palm.jpg b/public/images/hero/beach-palm.jpg new file mode 100644 index 0000000..2c6662b Binary files /dev/null and b/public/images/hero/beach-palm.jpg differ diff --git a/public/images/hero/beach-sunset.jpg b/public/images/hero/beach-sunset.jpg new file mode 100644 index 0000000..977cbc3 Binary files /dev/null and b/public/images/hero/beach-sunset.jpg differ diff --git a/public/images/showcase/certificate-design.jpg b/public/images/showcase/certificate-design.jpg new file mode 100644 index 0000000..503dbed Binary files /dev/null and b/public/images/showcase/certificate-design.jpg differ diff --git a/public/images/showcase/dashboard-screen.jpg b/public/images/showcase/dashboard-screen.jpg new file mode 100644 index 0000000..c8d22ca Binary files /dev/null and b/public/images/showcase/dashboard-screen.jpg differ diff --git a/public/images/showcase/family-vacation-joy.jpg b/public/images/showcase/family-vacation-joy.jpg new file mode 100644 index 0000000..14bbeeb Binary files /dev/null and b/public/images/showcase/family-vacation-joy.jpg differ diff --git a/public/images/showcase/infinity-pool-sunset.jpg b/public/images/showcase/infinity-pool-sunset.jpg new file mode 100644 index 0000000..7af312e Binary files /dev/null and b/public/images/showcase/infinity-pool-sunset.jpg differ diff --git a/public/images/showcase/resort-beachfront.jpg b/public/images/showcase/resort-beachfront.jpg new file mode 100644 index 0000000..df44740 Binary files /dev/null and b/public/images/showcase/resort-beachfront.jpg differ diff --git a/public/llms.txt b/public/llms.txt new file mode 100644 index 0000000..234d998 --- /dev/null +++ b/public/llms.txt @@ -0,0 +1,64 @@ +# hi2b.com — Mexico Paradise Vacations + +> All-inclusive Mexico vacation certificates. 5 days and 4 nights at a luxury beachfront resort in Cancun, Cabo, Riviera Maya, or Puerto Vallarta. 2 adults plus kids under 12 free. Payment plans from $39/month. + +Mexico Paradise Vacations (hi2b.com) is a travel-certificate company. Each certificate covers a 5-day / 4-night all-inclusive stay for 2 adults at a real beachfront resort in one of four Mexican destinations, with kids under 12 staying free. The certificates are sold on a monthly payment plan or as a discounted one-time payment. Customers redeem the certificate by booking dates through the client portal after the first payment. + +## Key pages + +- [Home](https://hi2b.com/): Main landing page +- [Pay](https://hi2b.com/pay): Direct payment page (used for phone sales) +- [Affiliate Portal](https://hi2b.com/affiliate): Affiliate sign-in and dashboard +- [Client Portal](https://hi2b.com/dashboard): Customer login (certificate, billing, bookings) +- [Privacy Policy](https://hi2b.com/privacy) +- [Terms of Service](https://hi2b.com/terms) + +## Landing pages + +- [Golden Hour](https://hi2b.com/lp/golden-hour): Escape to paradise. 5 days, 4 nights all-inclusive Mexico vacation for just $39/month. +- [Midnight Tropical](https://hi2b.com/lp/midnight-tropical): Limited spots remaining. Claim your all-inclusive Mexico getaway. +- [Passport Stamp](https://hi2b.com/lp/passport-stamp): Adventure awaits. All-inclusive Mexico vacation certificates from $39/month. +- [Crystal Clear](https://hi2b.com/lp/crystal-clear): All-inclusive Mexico vacation. Simple pricing. Incredible value. +- [Fiesta](https://hi2b.com/lp/fiesta): Celebrate life with an all-inclusive Mexico vacation from $39/month. +- [The Closer](https://hi2b.com/lp/the-closer): The math doesn\ +- [Resort Preview](https://hi2b.com/lp/resort-preview): Preview luxury resorts in Cancun, Cabo, Riviera Maya & Puerto Vallarta. +- [Split Decision](https://hi2b.com/lp/split-decision): Cancun or Cabo? Pick your dream destination. All-inclusive from $39/month. +- [Calculator](https://hi2b.com/lp/calculator): See exactly how much you save vs. booking direct. The math speaks for itself. +- [Countdown](https://hi2b.com/lp/countdown): Limited time offer. Claim your all-inclusive Mexico vacation before it\ +- [The Guide](https://hi2b.com/lp/the-guide): Free guide: 5 secrets to luxury Mexico vacations on a budget. +- [Dreamboard](https://hi2b.com/lp/dreamboard): Visualize your perfect Mexico getaway. Get the free planning guide. +- [Quiz Funnel](https://hi2b.com/lp/quiz-funnel): Take the quiz to find your ideal Mexico vacation destination. +- [Social Wall](https://hi2b.com/lp/social-wall): See what real travelers are saying about Mexico Paradise Vacations. +- [Savings Journal](https://hi2b.com/lp/savings-journal): $1.30/day is less than your latte. Start saving for paradise. +- [Couples Retreat](https://hi2b.com/lp/couples-retreat): Plan the romantic Mexico getaway you\ +- [Postcards](https://hi2b.com/lp/postcards): Send yourself a postcard from the future. Mexico awaits. +- [Stress Relief](https://hi2b.com/lp/stress-relief): Escape the stress. All-inclusive Mexico vacation for your wellbeing. +- [Foodie Paradise](https://hi2b.com/lp/foodie-paradise): All-inclusive dining at world-class Mexico resorts. From $39/month. +- [Family Escape](https://hi2b.com/lp/family-escape): Family-friendly all-inclusive Mexico vacations from $39/month. +- [Last Chance](https://hi2b.com/lp/last-chance): This price disappears in minutes. All-inclusive Mexico vacation. +- [The Proof](https://hi2b.com/lp/proof): Watch real TikTok videos from travelers at our resorts. +- [VIP Access](https://hi2b.com/lp/vip-access): You\ +- [One Tap](https://hi2b.com/lp/one-tap): The simplest way to book your dream Mexico vacation. $39/mo. +- [FOMO Feed](https://hi2b.com/lp/fomo-feed): See what you\ +- [Price Lock](https://hi2b.com/lp/price-lock): After this timer expires, the price goes up. Lock it in now. +- [Before & After](https://hi2b.com/lp/before-after): See the transformation. Desk to beach in one payment. +- [Risk Free](https://hi2b.com/lp/risk-free): Try it risk-free. If you\ +- [Speed Deal](https://hi2b.com/lp/speed-deal): This deal self-destructs. All-inclusive Mexico from $1.30/day. +- [Influencer](https://hi2b.com/lp/influencer): The vacation deal going viral. Watch the videos, book the trip. +- [Bucket List](https://hi2b.com/lp/bucket-list): Life\ +- [Deal Breaker](https://hi2b.com/lp/deal-breaker): Compare us to any travel site. We win every time. +- [Escape Plan](https://hi2b.com/lp/escape-plan): Download your free Mexico vacation planning guide. +- [TikTok Vibes](https://hi2b.com/lp/tiktok-vibes): See why this deal is going viral. Get the free insider guide. +- [No Brainer](https://hi2b.com/lp/no-brainer): $1.30/day for luxury. We\ +- [Weekend Escape](https://hi2b.com/lp/weekend-escape): 5 days that will change how you think about vacations. +- [Trust Fall](https://hi2b.com/lp/trust-fall): Real reviews, real videos, real people. See for yourself. +- [Sunrise](https://hi2b.com/lp/sunrise): Imagine waking up to ocean views. Get the free travel guide. +- [Adrenaline](https://hi2b.com/lp/adrenaline): Ziplines, cenotes, ruins — plus all-inclusive luxury. From $39/mo. +- [Golden Ticket](https://hi2b.com/lp/golden-ticket): This exclusive offer won\ +- [Seat Reserved](https://hi2b.com/lp/seat-reserved): Your paradise seat is confirmed. Lock in $29/mo before the countdown ends. +- [VIP Pass](https://hi2b.com/lp/vip-pass): Private concierge, lifetime rebooking, guest upgrades. VIP cohort closes at midnight. +- [Real Traveler](https://hi2b.com/lp/real-traveler): Real traveler · Day 4 in Mexico · Same resort her friends paid $2,800 for. See her 20-second story. + +## Optional + +- [Sitemap](https://hi2b.com/sitemap.xml) \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt index 6018e70..3f3b67a 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,14 +1,134 @@ +# hi2b.com — Mexico Paradise Vacations +# All crawlers welcome, including AI / LLM training and assistant bots. + +User-agent: * +Allow: / + +# --- Search engines --- User-agent: Googlebot Allow: / User-agent: Bingbot Allow: / +User-agent: DuckDuckBot +Allow: / + +User-agent: Baiduspider +Allow: / + +User-agent: YandexBot +Allow: / + +# --- Social / link previews --- User-agent: Twitterbot Allow: / User-agent: facebookexternalhit Allow: / -User-agent: * +User-agent: LinkedInBot +Allow: / + +User-agent: Slackbot +Allow: / + +User-agent: Discordbot +Allow: / + +# --- OpenAI --- +User-agent: GPTBot +Allow: / + +User-agent: ChatGPT-User +Allow: / + +User-agent: OAI-SearchBot +Allow: / + +# --- Anthropic --- +User-agent: anthropic-ai +Allow: / + +User-agent: ClaudeBot +Allow: / + +User-agent: Claude-Web +Allow: / + +User-agent: Claude-User +Allow: / + +User-agent: Claude-SearchBot +Allow: / + +# --- Google AI --- +User-agent: Google-Extended +Allow: / + +User-agent: GoogleOther +Allow: / + +# --- Apple --- +User-agent: Applebot +Allow: / + +User-agent: Applebot-Extended +Allow: / + +# --- Perplexity --- +User-agent: PerplexityBot +Allow: / + +User-agent: Perplexity-User +Allow: / + +# --- Meta / Facebook --- +User-agent: Meta-ExternalAgent +Allow: / + +User-agent: Meta-ExternalFetcher +Allow: / + +User-agent: FacebookBot +Allow: / + +# --- ByteDance / TikTok --- +User-agent: Bytespider +Allow: / + +# --- Amazon --- +User-agent: Amazonbot +Allow: / + +# --- Cohere --- +User-agent: cohere-ai +Allow: / + +User-agent: cohere-training-data-crawler +Allow: / + +# --- Other AI assistants --- +User-agent: DuckAssistBot +Allow: / + +User-agent: YouBot +Allow: / + +User-agent: MistralAI-User Allow: / + +User-agent: Diffbot +Allow: / + +User-agent: ImagesiftBot +Allow: / + +User-agent: omgilibot +Allow: / + +# --- Common Crawl (used for many LLM training corpora) --- +User-agent: CCBot +Allow: / + +Sitemap: https://hi2b.com/sitemap.xml diff --git a/public/tiktok/7598715055793868052.jpg b/public/tiktok/7598715055793868052.jpg new file mode 100644 index 0000000..115e045 Binary files /dev/null and b/public/tiktok/7598715055793868052.jpg differ diff --git a/public/tiktok/7598726765644696853.jpg b/public/tiktok/7598726765644696853.jpg new file mode 100644 index 0000000..8982ca2 Binary files /dev/null and b/public/tiktok/7598726765644696853.jpg differ diff --git a/public/tiktok/7598734004359007509.jpg b/public/tiktok/7598734004359007509.jpg new file mode 100644 index 0000000..6e5bc25 Binary files /dev/null and b/public/tiktok/7598734004359007509.jpg differ diff --git a/public/tiktok/7599817155001044231.jpg b/public/tiktok/7599817155001044231.jpg new file mode 100644 index 0000000..ecd56a7 Binary files /dev/null and b/public/tiktok/7599817155001044231.jpg differ diff --git a/public/tiktok/7599823703538453768.jpg b/public/tiktok/7599823703538453768.jpg new file mode 100644 index 0000000..3a879cb Binary files /dev/null and b/public/tiktok/7599823703538453768.jpg differ diff --git a/public/tiktok/7599836054589394194.jpg b/public/tiktok/7599836054589394194.jpg new file mode 100644 index 0000000..5b7a7e6 Binary files /dev/null and b/public/tiktok/7599836054589394194.jpg differ diff --git a/public/tiktok/7600189653106380039.jpg b/public/tiktok/7600189653106380039.jpg new file mode 100644 index 0000000..1e10840 Binary files /dev/null and b/public/tiktok/7600189653106380039.jpg differ diff --git a/public/tiktok/7600194855343541522.jpg b/public/tiktok/7600194855343541522.jpg new file mode 100644 index 0000000..ab28297 Binary files /dev/null and b/public/tiktok/7600194855343541522.jpg differ diff --git a/public/tiktok/7600201175933177109.jpg b/public/tiktok/7600201175933177109.jpg new file mode 100644 index 0000000..e9ac5e6 Binary files /dev/null and b/public/tiktok/7600201175933177109.jpg differ diff --git a/public/tiktok/7600205520087043335.jpg b/public/tiktok/7600205520087043335.jpg new file mode 100644 index 0000000..004268b Binary files /dev/null and b/public/tiktok/7600205520087043335.jpg differ diff --git a/public/tiktok/7600207352259808532.jpg b/public/tiktok/7600207352259808532.jpg new file mode 100644 index 0000000..e2d4c45 Binary files /dev/null and b/public/tiktok/7600207352259808532.jpg differ diff --git a/public/tiktok/7600210082596539669.jpg b/public/tiktok/7600210082596539669.jpg new file mode 100644 index 0000000..1f8ca73 Binary files /dev/null and b/public/tiktok/7600210082596539669.jpg differ diff --git a/public/tiktok/7600210549238009106.jpg b/public/tiktok/7600210549238009106.jpg new file mode 100644 index 0000000..f2a0575 Binary files /dev/null and b/public/tiktok/7600210549238009106.jpg differ diff --git a/public/tiktok/7600213209684970772.jpg b/public/tiktok/7600213209684970772.jpg new file mode 100644 index 0000000..452c16a Binary files /dev/null and b/public/tiktok/7600213209684970772.jpg differ diff --git a/public/tiktok/7600559301060578581.jpg b/public/tiktok/7600559301060578581.jpg new file mode 100644 index 0000000..beea6da Binary files /dev/null and b/public/tiktok/7600559301060578581.jpg differ diff --git a/public/tiktok/7600559846118690055.jpg b/public/tiktok/7600559846118690055.jpg new file mode 100644 index 0000000..93eb67d Binary files /dev/null and b/public/tiktok/7600559846118690055.jpg differ diff --git a/public/tiktok/7600564652304518418.jpg b/public/tiktok/7600564652304518418.jpg new file mode 100644 index 0000000..73decfe Binary files /dev/null and b/public/tiktok/7600564652304518418.jpg differ diff --git a/public/tiktok/7600564867724021013.jpg b/public/tiktok/7600564867724021013.jpg new file mode 100644 index 0000000..e5624bc Binary files /dev/null and b/public/tiktok/7600564867724021013.jpg differ diff --git a/public/tiktok/7600568620103650578.jpg b/public/tiktok/7600568620103650578.jpg new file mode 100644 index 0000000..af83e56 Binary files /dev/null and b/public/tiktok/7600568620103650578.jpg differ diff --git a/public/tiktok/7600574564598385928.jpg b/public/tiktok/7600574564598385928.jpg new file mode 100644 index 0000000..89563b7 Binary files /dev/null and b/public/tiktok/7600574564598385928.jpg differ diff --git a/public/tiktok/7600579435082812679.jpg b/public/tiktok/7600579435082812679.jpg new file mode 100644 index 0000000..64668b1 Binary files /dev/null and b/public/tiktok/7600579435082812679.jpg differ diff --git a/public/tiktok/7600582098000383252.jpg b/public/tiktok/7600582098000383252.jpg new file mode 100644 index 0000000..95cca5d Binary files /dev/null and b/public/tiktok/7600582098000383252.jpg differ diff --git a/public/tiktok/7600582664717913351.jpg b/public/tiktok/7600582664717913351.jpg new file mode 100644 index 0000000..866e1ed Binary files /dev/null and b/public/tiktok/7600582664717913351.jpg differ diff --git a/public/tiktok/7600928850821860626.jpg b/public/tiktok/7600928850821860626.jpg new file mode 100644 index 0000000..95f0d18 Binary files /dev/null and b/public/tiktok/7600928850821860626.jpg differ diff --git a/public/tiktok/7600928959915756820.jpg b/public/tiktok/7600928959915756820.jpg new file mode 100644 index 0000000..1b78163 Binary files /dev/null and b/public/tiktok/7600928959915756820.jpg differ diff --git a/public/tiktok/7600931800495475975.jpg b/public/tiktok/7600931800495475975.jpg new file mode 100644 index 0000000..64ca383 Binary files /dev/null and b/public/tiktok/7600931800495475975.jpg differ diff --git a/public/tiktok/7600936171354557704.jpg b/public/tiktok/7600936171354557704.jpg new file mode 100644 index 0000000..3ccbc57 Binary files /dev/null and b/public/tiktok/7600936171354557704.jpg differ diff --git a/public/tiktok/7600940014209371400.jpg b/public/tiktok/7600940014209371400.jpg new file mode 100644 index 0000000..1b48917 Binary files /dev/null and b/public/tiktok/7600940014209371400.jpg differ diff --git a/public/tiktok/7600948478646242581.jpg b/public/tiktok/7600948478646242581.jpg new file mode 100644 index 0000000..bab988d Binary files /dev/null and b/public/tiktok/7600948478646242581.jpg differ diff --git a/public/tiktok/7600949627860307208.jpg b/public/tiktok/7600949627860307208.jpg new file mode 100644 index 0000000..a6bf7ff Binary files /dev/null and b/public/tiktok/7600949627860307208.jpg differ diff --git a/public/tiktok/7600953259527818517.jpg b/public/tiktok/7600953259527818517.jpg new file mode 100644 index 0000000..aa03186 Binary files /dev/null and b/public/tiktok/7600953259527818517.jpg differ diff --git a/public/tiktok/7600953943153331463.jpg b/public/tiktok/7600953943153331463.jpg new file mode 100644 index 0000000..fd29b46 Binary files /dev/null and b/public/tiktok/7600953943153331463.jpg differ diff --git a/public/tiktok/7601298265182637320.jpg b/public/tiktok/7601298265182637320.jpg new file mode 100644 index 0000000..0326064 Binary files /dev/null and b/public/tiktok/7601298265182637320.jpg differ diff --git a/public/tiktok/7601301018957106440.jpg b/public/tiktok/7601301018957106440.jpg new file mode 100644 index 0000000..9875a87 Binary files /dev/null and b/public/tiktok/7601301018957106440.jpg differ diff --git a/public/tiktok/7601302133505264916.jpg b/public/tiktok/7601302133505264916.jpg new file mode 100644 index 0000000..4929f44 Binary files /dev/null and b/public/tiktok/7601302133505264916.jpg differ diff --git a/public/tiktok/7601306996272270599.jpg b/public/tiktok/7601306996272270599.jpg new file mode 100644 index 0000000..0b06745 Binary files /dev/null and b/public/tiktok/7601306996272270599.jpg differ diff --git a/public/tiktok/7601307252854721813.jpg b/public/tiktok/7601307252854721813.jpg new file mode 100644 index 0000000..8155147 Binary files /dev/null and b/public/tiktok/7601307252854721813.jpg differ diff --git a/public/tiktok/7601314019064040724.jpg b/public/tiktok/7601314019064040724.jpg new file mode 100644 index 0000000..d4d99d1 Binary files /dev/null and b/public/tiktok/7601314019064040724.jpg differ diff --git a/public/tiktok/7601315168517180679.jpg b/public/tiktok/7601315168517180679.jpg new file mode 100644 index 0000000..1672b08 Binary files /dev/null and b/public/tiktok/7601315168517180679.jpg differ diff --git a/public/tiktok/7601319181895814421.jpg b/public/tiktok/7601319181895814421.jpg new file mode 100644 index 0000000..4641aef Binary files /dev/null and b/public/tiktok/7601319181895814421.jpg differ diff --git a/public/tiktok/7601322386176134407.jpg b/public/tiktok/7601322386176134407.jpg new file mode 100644 index 0000000..8b4f170 Binary files /dev/null and b/public/tiktok/7601322386176134407.jpg differ diff --git a/public/tiktok/7601324989383527698.jpg b/public/tiktok/7601324989383527698.jpg new file mode 100644 index 0000000..eae76e1 Binary files /dev/null and b/public/tiktok/7601324989383527698.jpg differ diff --git a/public/tiktok/7601325166483737877.jpg b/public/tiktok/7601325166483737877.jpg new file mode 100644 index 0000000..4ace3ef Binary files /dev/null and b/public/tiktok/7601325166483737877.jpg differ diff --git a/public/tiktok/7602412252343438599.jpg b/public/tiktok/7602412252343438599.jpg new file mode 100644 index 0000000..57c9a21 Binary files /dev/null and b/public/tiktok/7602412252343438599.jpg differ diff --git a/public/tiktok/7602416103356239111.jpg b/public/tiktok/7602416103356239111.jpg new file mode 100644 index 0000000..d1597d8 Binary files /dev/null and b/public/tiktok/7602416103356239111.jpg differ diff --git a/public/tiktok/7602416902236835093.jpg b/public/tiktok/7602416902236835093.jpg new file mode 100644 index 0000000..841ea6b Binary files /dev/null and b/public/tiktok/7602416902236835093.jpg differ diff --git a/public/tiktok/7602421070645169429.jpg b/public/tiktok/7602421070645169429.jpg new file mode 100644 index 0000000..ed76452 Binary files /dev/null and b/public/tiktok/7602421070645169429.jpg differ diff --git a/public/tiktok/7602422161747283207.jpg b/public/tiktok/7602422161747283207.jpg new file mode 100644 index 0000000..ff8d27f Binary files /dev/null and b/public/tiktok/7602422161747283207.jpg differ diff --git a/public/tiktok/7602427461330029844.jpg b/public/tiktok/7602427461330029844.jpg new file mode 100644 index 0000000..4b901c1 Binary files /dev/null and b/public/tiktok/7602427461330029844.jpg differ diff --git a/public/tiktok/7602427669245889800.jpg b/public/tiktok/7602427669245889800.jpg new file mode 100644 index 0000000..177660b Binary files /dev/null and b/public/tiktok/7602427669245889800.jpg differ diff --git a/public/tiktok/7602432094261841172.jpg b/public/tiktok/7602432094261841172.jpg new file mode 100644 index 0000000..1b1bcad Binary files /dev/null and b/public/tiktok/7602432094261841172.jpg differ diff --git a/public/tiktok/7602432924708834568.jpg b/public/tiktok/7602432924708834568.jpg new file mode 100644 index 0000000..f1ca966 Binary files /dev/null and b/public/tiktok/7602432924708834568.jpg differ diff --git a/public/tiktok/7602435246679739668.jpg b/public/tiktok/7602435246679739668.jpg new file mode 100644 index 0000000..fae08de Binary files /dev/null and b/public/tiktok/7602435246679739668.jpg differ diff --git a/public/tiktok/7602436296635632904.jpg b/public/tiktok/7602436296635632904.jpg new file mode 100644 index 0000000..a17161a Binary files /dev/null and b/public/tiktok/7602436296635632904.jpg differ diff --git a/public/tiktok/7602437874096639253.jpg b/public/tiktok/7602437874096639253.jpg new file mode 100644 index 0000000..6c514e5 Binary files /dev/null and b/public/tiktok/7602437874096639253.jpg differ diff --git a/public/tiktok/7602783687364578567.jpg b/public/tiktok/7602783687364578567.jpg new file mode 100644 index 0000000..90922ad Binary files /dev/null and b/public/tiktok/7602783687364578567.jpg differ diff --git a/public/tiktok/7602788687109328135.jpg b/public/tiktok/7602788687109328135.jpg new file mode 100644 index 0000000..a2bb938 Binary files /dev/null and b/public/tiktok/7602788687109328135.jpg differ diff --git a/public/tiktok/7602793251241856263.jpg b/public/tiktok/7602793251241856263.jpg new file mode 100644 index 0000000..afaf489 Binary files /dev/null and b/public/tiktok/7602793251241856263.jpg differ diff --git a/public/tiktok/7602797860169567506.jpg b/public/tiktok/7602797860169567506.jpg new file mode 100644 index 0000000..7e17933 Binary files /dev/null and b/public/tiktok/7602797860169567506.jpg differ diff --git a/public/tiktok/7602801434245074194.jpg b/public/tiktok/7602801434245074194.jpg new file mode 100644 index 0000000..5d30325 Binary files /dev/null and b/public/tiktok/7602801434245074194.jpg differ diff --git a/public/tiktok/7602808291353201938.jpg b/public/tiktok/7602808291353201938.jpg new file mode 100644 index 0000000..be020a6 Binary files /dev/null and b/public/tiktok/7602808291353201938.jpg differ diff --git a/public/tiktok/7603155425307184402.jpg b/public/tiktok/7603155425307184402.jpg new file mode 100644 index 0000000..f187417 Binary files /dev/null and b/public/tiktok/7603155425307184402.jpg differ diff --git a/public/tiktok/7603159313737190664.jpg b/public/tiktok/7603159313737190664.jpg new file mode 100644 index 0000000..2795997 Binary files /dev/null and b/public/tiktok/7603159313737190664.jpg differ diff --git a/public/tiktok/7603162462015245576.jpg b/public/tiktok/7603162462015245576.jpg new file mode 100644 index 0000000..0b92b62 Binary files /dev/null and b/public/tiktok/7603162462015245576.jpg differ diff --git a/public/tiktok/7603170263777201416.jpg b/public/tiktok/7603170263777201416.jpg new file mode 100644 index 0000000..3c46cbe Binary files /dev/null and b/public/tiktok/7603170263777201416.jpg differ diff --git a/public/tiktok/7603172717289950472.jpg b/public/tiktok/7603172717289950472.jpg new file mode 100644 index 0000000..1562a11 Binary files /dev/null and b/public/tiktok/7603172717289950472.jpg differ diff --git a/public/tiktok/7603176846171098375.jpg b/public/tiktok/7603176846171098375.jpg new file mode 100644 index 0000000..c6335de Binary files /dev/null and b/public/tiktok/7603176846171098375.jpg differ diff --git a/public/tiktok/7603529785150672146.jpg b/public/tiktok/7603529785150672146.jpg new file mode 100644 index 0000000..5fb2d5f Binary files /dev/null and b/public/tiktok/7603529785150672146.jpg differ diff --git a/public/tiktok/7603533611102424328.jpg b/public/tiktok/7603533611102424328.jpg new file mode 100644 index 0000000..9cc6eec Binary files /dev/null and b/public/tiktok/7603533611102424328.jpg differ diff --git a/public/tiktok/7603541240449223943.jpg b/public/tiktok/7603541240449223943.jpg new file mode 100644 index 0000000..14c3a97 Binary files /dev/null and b/public/tiktok/7603541240449223943.jpg differ diff --git a/public/tiktok/7603542827678715156.jpg b/public/tiktok/7603542827678715156.jpg new file mode 100644 index 0000000..d3489a4 Binary files /dev/null and b/public/tiktok/7603542827678715156.jpg differ diff --git a/public/tiktok/7603547156028476693.jpg b/public/tiktok/7603547156028476693.jpg new file mode 100644 index 0000000..b192875 Binary files /dev/null and b/public/tiktok/7603547156028476693.jpg differ diff --git a/public/tiktok/7603547720111901970.jpg b/public/tiktok/7603547720111901970.jpg new file mode 100644 index 0000000..df6a68d Binary files /dev/null and b/public/tiktok/7603547720111901970.jpg differ diff --git a/public/tiktok/7603904300644961556.jpg b/public/tiktok/7603904300644961556.jpg new file mode 100644 index 0000000..3192b5c Binary files /dev/null and b/public/tiktok/7603904300644961556.jpg differ diff --git a/public/tiktok/7603905329964895495.jpg b/public/tiktok/7603905329964895495.jpg new file mode 100644 index 0000000..3259fd6 Binary files /dev/null and b/public/tiktok/7603905329964895495.jpg differ diff --git a/public/tiktok/7603907885462080775.jpg b/public/tiktok/7603907885462080775.jpg new file mode 100644 index 0000000..8432fa9 Binary files /dev/null and b/public/tiktok/7603907885462080775.jpg differ diff --git a/public/tiktok/7603915141607853332.jpg b/public/tiktok/7603915141607853332.jpg new file mode 100644 index 0000000..43abf56 Binary files /dev/null and b/public/tiktok/7603915141607853332.jpg differ diff --git a/public/tiktok/7603916425044036882.jpg b/public/tiktok/7603916425044036882.jpg new file mode 100644 index 0000000..61c9d30 Binary files /dev/null and b/public/tiktok/7603916425044036882.jpg differ diff --git a/public/tiktok/7603917143503113492.jpg b/public/tiktok/7603917143503113492.jpg new file mode 100644 index 0000000..4317427 Binary files /dev/null and b/public/tiktok/7603917143503113492.jpg differ diff --git a/public/tiktok/7603919277430721810.jpg b/public/tiktok/7603919277430721810.jpg new file mode 100644 index 0000000..246b574 Binary files /dev/null and b/public/tiktok/7603919277430721810.jpg differ diff --git a/public/tiktok/7603922127410056469.jpg b/public/tiktok/7603922127410056469.jpg new file mode 100644 index 0000000..7837280 Binary files /dev/null and b/public/tiktok/7603922127410056469.jpg differ diff --git a/public/tiktok/7605009292281859335.jpg b/public/tiktok/7605009292281859335.jpg new file mode 100644 index 0000000..b54528e Binary files /dev/null and b/public/tiktok/7605009292281859335.jpg differ diff --git a/public/tiktok/7605011840698944786.jpg b/public/tiktok/7605011840698944786.jpg new file mode 100644 index 0000000..73aa65f Binary files /dev/null and b/public/tiktok/7605011840698944786.jpg differ diff --git a/public/tiktok/7605011911742115093.jpg b/public/tiktok/7605011911742115093.jpg new file mode 100644 index 0000000..eccf8e6 Binary files /dev/null and b/public/tiktok/7605011911742115093.jpg differ diff --git a/public/tiktok/7605014829023251719.jpg b/public/tiktok/7605014829023251719.jpg new file mode 100644 index 0000000..cfed514 Binary files /dev/null and b/public/tiktok/7605014829023251719.jpg differ diff --git a/public/tiktok/7605017071356792084.jpg b/public/tiktok/7605017071356792084.jpg new file mode 100644 index 0000000..093cf59 Binary files /dev/null and b/public/tiktok/7605017071356792084.jpg differ diff --git a/public/tiktok/7605025108616350996.jpg b/public/tiktok/7605025108616350996.jpg new file mode 100644 index 0000000..7af1862 Binary files /dev/null and b/public/tiktok/7605025108616350996.jpg differ diff --git a/public/tiktok/7605027310227426567.jpg b/public/tiktok/7605027310227426567.jpg new file mode 100644 index 0000000..69bb4dc Binary files /dev/null and b/public/tiktok/7605027310227426567.jpg differ diff --git a/public/tiktok/7605030386606951687.jpg b/public/tiktok/7605030386606951687.jpg new file mode 100644 index 0000000..7594b36 Binary files /dev/null and b/public/tiktok/7605030386606951687.jpg differ diff --git a/public/tiktok/7605031715878341908.jpg b/public/tiktok/7605031715878341908.jpg new file mode 100644 index 0000000..a8e6bf0 Binary files /dev/null and b/public/tiktok/7605031715878341908.jpg differ diff --git a/public/tiktok/7605034148218113301.jpg b/public/tiktok/7605034148218113301.jpg new file mode 100644 index 0000000..c27efb5 Binary files /dev/null and b/public/tiktok/7605034148218113301.jpg differ diff --git a/public/tiktok/7605385078083980565.jpg b/public/tiktok/7605385078083980565.jpg new file mode 100644 index 0000000..ebdb77e Binary files /dev/null and b/public/tiktok/7605385078083980565.jpg differ diff --git a/public/tiktok/7605388929088441621.jpg b/public/tiktok/7605388929088441621.jpg new file mode 100644 index 0000000..85e515d Binary files /dev/null and b/public/tiktok/7605388929088441621.jpg differ diff --git a/public/tiktok/7605393075606850836.jpg b/public/tiktok/7605393075606850836.jpg new file mode 100644 index 0000000..d9867cb Binary files /dev/null and b/public/tiktok/7605393075606850836.jpg differ diff --git a/public/tiktok/7605403420501134613.jpg b/public/tiktok/7605403420501134613.jpg new file mode 100644 index 0000000..610b873 Binary files /dev/null and b/public/tiktok/7605403420501134613.jpg differ diff --git a/public/tiktok/7605763423334337813.jpg b/public/tiktok/7605763423334337813.jpg new file mode 100644 index 0000000..e0d54df Binary files /dev/null and b/public/tiktok/7605763423334337813.jpg differ diff --git a/public/tiktok/7605765455831141653.jpg b/public/tiktok/7605765455831141653.jpg new file mode 100644 index 0000000..21b9cec Binary files /dev/null and b/public/tiktok/7605765455831141653.jpg differ diff --git a/public/tiktok/7605771533738380564.jpg b/public/tiktok/7605771533738380564.jpg new file mode 100644 index 0000000..e286378 Binary files /dev/null and b/public/tiktok/7605771533738380564.jpg differ diff --git a/public/tiktok/7605774409177107732.jpg b/public/tiktok/7605774409177107732.jpg new file mode 100644 index 0000000..e76d78f Binary files /dev/null and b/public/tiktok/7605774409177107732.jpg differ diff --git a/public/tiktok/7605777886037003541.jpg b/public/tiktok/7605777886037003541.jpg new file mode 100644 index 0000000..e4fc2f1 Binary files /dev/null and b/public/tiktok/7605777886037003541.jpg differ diff --git a/public/tiktok/7606128084961545493.jpg b/public/tiktok/7606128084961545493.jpg new file mode 100644 index 0000000..660fdb3 Binary files /dev/null and b/public/tiktok/7606128084961545493.jpg differ diff --git a/public/tiktok/7606131787672079636.jpg b/public/tiktok/7606131787672079636.jpg new file mode 100644 index 0000000..a2adbf2 Binary files /dev/null and b/public/tiktok/7606131787672079636.jpg differ diff --git a/public/tiktok/7606137412191882517.jpg b/public/tiktok/7606137412191882517.jpg new file mode 100644 index 0000000..2dc8660 Binary files /dev/null and b/public/tiktok/7606137412191882517.jpg differ diff --git a/public/tiktok/7606141425520053524.jpg b/public/tiktok/7606141425520053524.jpg new file mode 100644 index 0000000..44b3aee Binary files /dev/null and b/public/tiktok/7606141425520053524.jpg differ diff --git a/public/tiktok/7606146061446417685.jpg b/public/tiktok/7606146061446417685.jpg new file mode 100644 index 0000000..610b873 Binary files /dev/null and b/public/tiktok/7606146061446417685.jpg differ diff --git a/public/tiktok/7608360935576603922.jpg b/public/tiktok/7608360935576603922.jpg new file mode 100644 index 0000000..23c9047 Binary files /dev/null and b/public/tiktok/7608360935576603922.jpg differ diff --git a/public/tiktok/7608725450415770888.jpg b/public/tiktok/7608725450415770888.jpg new file mode 100644 index 0000000..065d68d Binary files /dev/null and b/public/tiktok/7608725450415770888.jpg differ diff --git a/public/tiktok/7608727936786648338.jpg b/public/tiktok/7608727936786648338.jpg new file mode 100644 index 0000000..4b3f950 Binary files /dev/null and b/public/tiktok/7608727936786648338.jpg differ diff --git a/public/tiktok/7608739596989451527.jpg b/public/tiktok/7608739596989451527.jpg new file mode 100644 index 0000000..0f3b574 Binary files /dev/null and b/public/tiktok/7608739596989451527.jpg differ diff --git a/public/tiktok/7608741747623070983.jpg b/public/tiktok/7608741747623070983.jpg new file mode 100644 index 0000000..504a73f Binary files /dev/null and b/public/tiktok/7608741747623070983.jpg differ diff --git a/public/tiktok/7609095709073263880.jpg b/public/tiktok/7609095709073263880.jpg new file mode 100644 index 0000000..8bd32da Binary files /dev/null and b/public/tiktok/7609095709073263880.jpg differ diff --git a/public/tiktok/7609098109934767367.jpg b/public/tiktok/7609098109934767367.jpg new file mode 100644 index 0000000..3a39557 Binary files /dev/null and b/public/tiktok/7609098109934767367.jpg differ diff --git a/public/tiktok/7609100427375725832.jpg b/public/tiktok/7609100427375725832.jpg new file mode 100644 index 0000000..056da59 Binary files /dev/null and b/public/tiktok/7609100427375725832.jpg differ diff --git a/public/tiktok/7609113519203667218.jpg b/public/tiktok/7609113519203667218.jpg new file mode 100644 index 0000000..a47ef48 Binary files /dev/null and b/public/tiktok/7609113519203667218.jpg differ diff --git a/public/tiktok/7609115330388053256.jpg b/public/tiktok/7609115330388053256.jpg new file mode 100644 index 0000000..213e2eb Binary files /dev/null and b/public/tiktok/7609115330388053256.jpg differ diff --git a/public/tiktok/7609117013834845447.jpg b/public/tiktok/7609117013834845447.jpg new file mode 100644 index 0000000..892e3ad Binary files /dev/null and b/public/tiktok/7609117013834845447.jpg differ diff --git a/public/tiktok/7610205552127266066.jpg b/public/tiktok/7610205552127266066.jpg new file mode 100644 index 0000000..2504524 Binary files /dev/null and b/public/tiktok/7610205552127266066.jpg differ diff --git a/public/tiktok/7610212093278555413.jpg b/public/tiktok/7610212093278555413.jpg new file mode 100644 index 0000000..cc62995 Binary files /dev/null and b/public/tiktok/7610212093278555413.jpg differ diff --git a/public/tiktok/7610214043680345352.jpg b/public/tiktok/7610214043680345352.jpg new file mode 100644 index 0000000..29e9588 Binary files /dev/null and b/public/tiktok/7610214043680345352.jpg differ diff --git a/public/tiktok/7610214868418956565.jpg b/public/tiktok/7610214868418956565.jpg new file mode 100644 index 0000000..f312849 Binary files /dev/null and b/public/tiktok/7610214868418956565.jpg differ diff --git a/public/tiktok/7610220494742211858.jpg b/public/tiktok/7610220494742211858.jpg new file mode 100644 index 0000000..dada332 Binary files /dev/null and b/public/tiktok/7610220494742211858.jpg differ diff --git a/public/tiktok/7610225854345399573.jpg b/public/tiktok/7610225854345399573.jpg new file mode 100644 index 0000000..ff8a1da Binary files /dev/null and b/public/tiktok/7610225854345399573.jpg differ diff --git a/public/tiktok/7610226305556221191.jpg b/public/tiktok/7610226305556221191.jpg new file mode 100644 index 0000000..033adf2 Binary files /dev/null and b/public/tiktok/7610226305556221191.jpg differ diff --git a/public/tiktok/7610228483909782791.jpg b/public/tiktok/7610228483909782791.jpg new file mode 100644 index 0000000..4c6f107 Binary files /dev/null and b/public/tiktok/7610228483909782791.jpg differ diff --git a/public/tiktok/7610229016678796565.jpg b/public/tiktok/7610229016678796565.jpg new file mode 100644 index 0000000..9884212 Binary files /dev/null and b/public/tiktok/7610229016678796565.jpg differ diff --git a/public/tiktok/7610582097748577544.jpg b/public/tiktok/7610582097748577544.jpg new file mode 100644 index 0000000..733b56c Binary files /dev/null and b/public/tiktok/7610582097748577544.jpg differ diff --git a/public/tiktok/7610586339880242453.jpg b/public/tiktok/7610586339880242453.jpg new file mode 100644 index 0000000..410546a Binary files /dev/null and b/public/tiktok/7610586339880242453.jpg differ diff --git a/public/tiktok/7610591010338458901.jpg b/public/tiktok/7610591010338458901.jpg new file mode 100644 index 0000000..c7b9da9 Binary files /dev/null and b/public/tiktok/7610591010338458901.jpg differ diff --git a/public/tiktok/7610593736338197780.jpg b/public/tiktok/7610593736338197780.jpg new file mode 100644 index 0000000..1282d7d Binary files /dev/null and b/public/tiktok/7610593736338197780.jpg differ diff --git a/public/tiktok/7610594867172920584.jpg b/public/tiktok/7610594867172920584.jpg new file mode 100644 index 0000000..da9f9d4 Binary files /dev/null and b/public/tiktok/7610594867172920584.jpg differ diff --git a/public/tiktok/7610596527475608853.jpg b/public/tiktok/7610596527475608853.jpg new file mode 100644 index 0000000..20e6aac Binary files /dev/null and b/public/tiktok/7610596527475608853.jpg differ diff --git a/public/tiktok/7610599550029516053.jpg b/public/tiktok/7610599550029516053.jpg new file mode 100644 index 0000000..ecfdff8 Binary files /dev/null and b/public/tiktok/7610599550029516053.jpg differ diff --git a/public/tiktok/7610608531678530823.jpg b/public/tiktok/7610608531678530823.jpg new file mode 100644 index 0000000..2e1e224 Binary files /dev/null and b/public/tiktok/7610608531678530823.jpg differ diff --git a/public/tiktok/7611326035031788818.jpg b/public/tiktok/7611326035031788818.jpg new file mode 100644 index 0000000..9c89593 Binary files /dev/null and b/public/tiktok/7611326035031788818.jpg differ diff --git a/public/tiktok/7611328364732845320.jpg b/public/tiktok/7611328364732845320.jpg new file mode 100644 index 0000000..d41f7a2 Binary files /dev/null and b/public/tiktok/7611328364732845320.jpg differ diff --git a/public/tiktok/7611330525692841224.jpg b/public/tiktok/7611330525692841224.jpg new file mode 100644 index 0000000..3d7322e Binary files /dev/null and b/public/tiktok/7611330525692841224.jpg differ diff --git a/public/tiktok/7611332584718568711.jpg b/public/tiktok/7611332584718568711.jpg new file mode 100644 index 0000000..27417f9 Binary files /dev/null and b/public/tiktok/7611332584718568711.jpg differ diff --git a/public/tiktok/7611333495373204756.jpg b/public/tiktok/7611333495373204756.jpg new file mode 100644 index 0000000..33918f3 Binary files /dev/null and b/public/tiktok/7611333495373204756.jpg differ diff --git a/public/tiktok/7611333628567571732.jpg b/public/tiktok/7611333628567571732.jpg new file mode 100644 index 0000000..71a560e Binary files /dev/null and b/public/tiktok/7611333628567571732.jpg differ diff --git a/public/tiktok/7611333870310460693.jpg b/public/tiktok/7611333870310460693.jpg new file mode 100644 index 0000000..d6c98de Binary files /dev/null and b/public/tiktok/7611333870310460693.jpg differ diff --git a/public/tiktok/7611334042528533780.jpg b/public/tiktok/7611334042528533780.jpg new file mode 100644 index 0000000..00c1512 Binary files /dev/null and b/public/tiktok/7611334042528533780.jpg differ diff --git a/public/tiktok/7611334215837256981.jpg b/public/tiktok/7611334215837256981.jpg new file mode 100644 index 0000000..610b873 Binary files /dev/null and b/public/tiktok/7611334215837256981.jpg differ diff --git a/public/tiktok/7611334414362004743.jpg b/public/tiktok/7611334414362004743.jpg new file mode 100644 index 0000000..b543a7f Binary files /dev/null and b/public/tiktok/7611334414362004743.jpg differ diff --git a/public/tiktok/7611694991936736530.jpg b/public/tiktok/7611694991936736530.jpg new file mode 100644 index 0000000..5dd1b6a Binary files /dev/null and b/public/tiktok/7611694991936736530.jpg differ diff --git a/public/tiktok/7611698597385522440.jpg b/public/tiktok/7611698597385522440.jpg new file mode 100644 index 0000000..b15edb2 Binary files /dev/null and b/public/tiktok/7611698597385522440.jpg differ diff --git a/public/tiktok/7611700828247706887.jpg b/public/tiktok/7611700828247706887.jpg new file mode 100644 index 0000000..b0a1767 Binary files /dev/null and b/public/tiktok/7611700828247706887.jpg differ diff --git a/public/tiktok/7611702983197314311.jpg b/public/tiktok/7611702983197314311.jpg new file mode 100644 index 0000000..2225037 Binary files /dev/null and b/public/tiktok/7611702983197314311.jpg differ diff --git a/public/tiktok/7611704941241060626.jpg b/public/tiktok/7611704941241060626.jpg new file mode 100644 index 0000000..f09e0c2 Binary files /dev/null and b/public/tiktok/7611704941241060626.jpg differ diff --git a/public/tiktok/7611712572877851925.jpg b/public/tiktok/7611712572877851925.jpg new file mode 100644 index 0000000..c3b9983 Binary files /dev/null and b/public/tiktok/7611712572877851925.jpg differ diff --git a/public/tiktok/7611712751257292053.jpg b/public/tiktok/7611712751257292053.jpg new file mode 100644 index 0000000..abde576 Binary files /dev/null and b/public/tiktok/7611712751257292053.jpg differ diff --git a/public/tiktok/7611712896271240469.jpg b/public/tiktok/7611712896271240469.jpg new file mode 100644 index 0000000..792479a Binary files /dev/null and b/public/tiktok/7611712896271240469.jpg differ diff --git a/public/tiktok/7611713028819537172.jpg b/public/tiktok/7611713028819537172.jpg new file mode 100644 index 0000000..dbc4006 Binary files /dev/null and b/public/tiktok/7611713028819537172.jpg differ diff --git a/public/tiktok/7611713202807754005.jpg b/public/tiktok/7611713202807754005.jpg new file mode 100644 index 0000000..0cc197f Binary files /dev/null and b/public/tiktok/7611713202807754005.jpg differ diff --git a/public/tiktok/7612811446468005138.jpg b/public/tiktok/7612811446468005138.jpg new file mode 100644 index 0000000..b647696 Binary files /dev/null and b/public/tiktok/7612811446468005138.jpg differ diff --git a/public/tiktok/7612813070410337554.jpg b/public/tiktok/7612813070410337554.jpg new file mode 100644 index 0000000..5ce54b4 Binary files /dev/null and b/public/tiktok/7612813070410337554.jpg differ diff --git a/public/tiktok/7612815416146103570.jpg b/public/tiktok/7612815416146103570.jpg new file mode 100644 index 0000000..e2a139d Binary files /dev/null and b/public/tiktok/7612815416146103570.jpg differ diff --git a/public/tiktok/7612818180699852050.jpg b/public/tiktok/7612818180699852050.jpg new file mode 100644 index 0000000..9a73fe9 Binary files /dev/null and b/public/tiktok/7612818180699852050.jpg differ diff --git a/public/tiktok/7612824588543626516.jpg b/public/tiktok/7612824588543626516.jpg new file mode 100644 index 0000000..e0a70dc Binary files /dev/null and b/public/tiktok/7612824588543626516.jpg differ diff --git a/public/tiktok/7612824727228271892.jpg b/public/tiktok/7612824727228271892.jpg new file mode 100644 index 0000000..77f6263 Binary files /dev/null and b/public/tiktok/7612824727228271892.jpg differ diff --git a/public/tiktok/7612824883743001877.jpg b/public/tiktok/7612824883743001877.jpg new file mode 100644 index 0000000..f05ba77 Binary files /dev/null and b/public/tiktok/7612824883743001877.jpg differ diff --git a/public/tiktok/7612825010587077908.jpg b/public/tiktok/7612825010587077908.jpg new file mode 100644 index 0000000..cef901c Binary files /dev/null and b/public/tiktok/7612825010587077908.jpg differ diff --git a/public/tiktok/7612825138291166485.jpg b/public/tiktok/7612825138291166485.jpg new file mode 100644 index 0000000..6d16600 Binary files /dev/null and b/public/tiktok/7612825138291166485.jpg differ diff --git a/public/tiktok/7613178679929228552.jpg b/public/tiktok/7613178679929228552.jpg new file mode 100644 index 0000000..234dc5c Binary files /dev/null and b/public/tiktok/7613178679929228552.jpg differ diff --git a/public/tiktok/7613180263983549703.jpg b/public/tiktok/7613180263983549703.jpg new file mode 100644 index 0000000..813e0e8 Binary files /dev/null and b/public/tiktok/7613180263983549703.jpg differ diff --git a/public/tiktok/7613182036366478610.jpg b/public/tiktok/7613182036366478610.jpg new file mode 100644 index 0000000..3e8d7b3 Binary files /dev/null and b/public/tiktok/7613182036366478610.jpg differ diff --git a/public/tiktok/7613183865204641031.jpg b/public/tiktok/7613183865204641031.jpg new file mode 100644 index 0000000..669f20a Binary files /dev/null and b/public/tiktok/7613183865204641031.jpg differ diff --git a/public/tiktok/7613185644747164935.jpg b/public/tiktok/7613185644747164935.jpg new file mode 100644 index 0000000..a2956d7 Binary files /dev/null and b/public/tiktok/7613185644747164935.jpg differ diff --git a/public/tiktok/7613607211008363784.jpg b/public/tiktok/7613607211008363784.jpg new file mode 100644 index 0000000..359cf01 Binary files /dev/null and b/public/tiktok/7613607211008363784.jpg differ diff --git a/public/tiktok/7613608501083917576.jpg b/public/tiktok/7613608501083917576.jpg new file mode 100644 index 0000000..e6f4926 Binary files /dev/null and b/public/tiktok/7613608501083917576.jpg differ diff --git a/public/tiktok/7613609941160529160.jpg b/public/tiktok/7613609941160529160.jpg new file mode 100644 index 0000000..337b68d Binary files /dev/null and b/public/tiktok/7613609941160529160.jpg differ diff --git a/public/tiktok/7613611646375693576.jpg b/public/tiktok/7613611646375693576.jpg new file mode 100644 index 0000000..68170e7 Binary files /dev/null and b/public/tiktok/7613611646375693576.jpg differ diff --git a/public/tiktok/7613613311300504850.jpg b/public/tiktok/7613613311300504850.jpg new file mode 100644 index 0000000..ff4dcc4 Binary files /dev/null and b/public/tiktok/7613613311300504850.jpg differ diff --git a/public/tiktok/7613938879388798228.jpg b/public/tiktok/7613938879388798228.jpg new file mode 100644 index 0000000..bbda2e5 Binary files /dev/null and b/public/tiktok/7613938879388798228.jpg differ diff --git a/public/tiktok/7613939116627086613.jpg b/public/tiktok/7613939116627086613.jpg new file mode 100644 index 0000000..43d54f2 Binary files /dev/null and b/public/tiktok/7613939116627086613.jpg differ diff --git a/public/tiktok/7613939329387220245.jpg b/public/tiktok/7613939329387220245.jpg new file mode 100644 index 0000000..88948b7 Binary files /dev/null and b/public/tiktok/7613939329387220245.jpg differ diff --git a/public/tiktok/7613939542713715989.jpg b/public/tiktok/7613939542713715989.jpg new file mode 100644 index 0000000..88948b7 Binary files /dev/null and b/public/tiktok/7613939542713715989.jpg differ diff --git a/public/tiktok/7613939774008593684.jpg b/public/tiktok/7613939774008593684.jpg new file mode 100644 index 0000000..e5f86c0 Binary files /dev/null and b/public/tiktok/7613939774008593684.jpg differ diff --git a/public/tiktok/7613974424663952658.jpg b/public/tiktok/7613974424663952658.jpg new file mode 100644 index 0000000..5dc22c5 Binary files /dev/null and b/public/tiktok/7613974424663952658.jpg differ diff --git a/public/tiktok/7613976159252008199.jpg b/public/tiktok/7613976159252008199.jpg new file mode 100644 index 0000000..9d73e97 Binary files /dev/null and b/public/tiktok/7613976159252008199.jpg differ diff --git a/public/tiktok/7613977546417310983.jpg b/public/tiktok/7613977546417310983.jpg new file mode 100644 index 0000000..cbe435b Binary files /dev/null and b/public/tiktok/7613977546417310983.jpg differ diff --git a/public/tiktok/7613978916574874887.jpg b/public/tiktok/7613978916574874887.jpg new file mode 100644 index 0000000..86dbb07 Binary files /dev/null and b/public/tiktok/7613978916574874887.jpg differ diff --git a/public/tiktok/7613982935712107794.jpg b/public/tiktok/7613982935712107794.jpg new file mode 100644 index 0000000..57dba06 Binary files /dev/null and b/public/tiktok/7613982935712107794.jpg differ diff --git a/public/tiktok/7614306570301738260.jpg b/public/tiktok/7614306570301738260.jpg new file mode 100644 index 0000000..df1a3c2 Binary files /dev/null and b/public/tiktok/7614306570301738260.jpg differ diff --git a/public/tiktok/7614306744541383957.jpg b/public/tiktok/7614306744541383957.jpg new file mode 100644 index 0000000..4aaf591 Binary files /dev/null and b/public/tiktok/7614306744541383957.jpg differ diff --git a/public/tiktok/7614306835511790868.jpg b/public/tiktok/7614306835511790868.jpg new file mode 100644 index 0000000..a5b2f01 Binary files /dev/null and b/public/tiktok/7614306835511790868.jpg differ diff --git a/public/tiktok/7614306978164264213.jpg b/public/tiktok/7614306978164264213.jpg new file mode 100644 index 0000000..d8972ce Binary files /dev/null and b/public/tiktok/7614306978164264213.jpg differ diff --git a/public/tiktok/7614307089007021332.jpg b/public/tiktok/7614307089007021332.jpg new file mode 100644 index 0000000..4d3ca7d Binary files /dev/null and b/public/tiktok/7614307089007021332.jpg differ diff --git a/public/tiktok/7615058166895709458.jpg b/public/tiktok/7615058166895709458.jpg new file mode 100644 index 0000000..1c2b9ff Binary files /dev/null and b/public/tiktok/7615058166895709458.jpg differ diff --git a/public/tiktok/7615059773691546887.jpg b/public/tiktok/7615059773691546887.jpg new file mode 100644 index 0000000..6163e7e Binary files /dev/null and b/public/tiktok/7615059773691546887.jpg differ diff --git a/public/tiktok/7615061277148187922.jpg b/public/tiktok/7615061277148187922.jpg new file mode 100644 index 0000000..ec90edc Binary files /dev/null and b/public/tiktok/7615061277148187922.jpg differ diff --git a/public/tiktok/7615063282382146824.jpg b/public/tiktok/7615063282382146824.jpg new file mode 100644 index 0000000..7fab61d Binary files /dev/null and b/public/tiktok/7615063282382146824.jpg differ diff --git a/public/tiktok/7615066189110185234.jpg b/public/tiktok/7615066189110185234.jpg new file mode 100644 index 0000000..037ff90 Binary files /dev/null and b/public/tiktok/7615066189110185234.jpg differ diff --git a/public/tiktok/7615417396140068116.jpg b/public/tiktok/7615417396140068116.jpg new file mode 100644 index 0000000..5e40352 Binary files /dev/null and b/public/tiktok/7615417396140068116.jpg differ diff --git a/public/tiktok/7615417606446632213.jpg b/public/tiktok/7615417606446632213.jpg new file mode 100644 index 0000000..5109443 Binary files /dev/null and b/public/tiktok/7615417606446632213.jpg differ diff --git a/public/tiktok/7615417898932243732.jpg b/public/tiktok/7615417898932243732.jpg new file mode 100644 index 0000000..3151660 Binary files /dev/null and b/public/tiktok/7615417898932243732.jpg differ diff --git a/public/tiktok/7615418100892192020.jpg b/public/tiktok/7615418100892192020.jpg new file mode 100644 index 0000000..3d1e022 Binary files /dev/null and b/public/tiktok/7615418100892192020.jpg differ diff --git a/public/tiktok/7615418268869741845.jpg b/public/tiktok/7615418268869741845.jpg new file mode 100644 index 0000000..ba824ba Binary files /dev/null and b/public/tiktok/7615418268869741845.jpg differ diff --git a/public/tiktok/7615447194316885256.jpg b/public/tiktok/7615447194316885256.jpg new file mode 100644 index 0000000..687bd7c Binary files /dev/null and b/public/tiktok/7615447194316885256.jpg differ diff --git a/public/tiktok/7615448674025442567.jpg b/public/tiktok/7615448674025442567.jpg new file mode 100644 index 0000000..1c8427c Binary files /dev/null and b/public/tiktok/7615448674025442567.jpg differ diff --git a/public/tiktok/7615450387285331208.jpg b/public/tiktok/7615450387285331208.jpg new file mode 100644 index 0000000..22139ea Binary files /dev/null and b/public/tiktok/7615450387285331208.jpg differ diff --git a/public/tiktok/7615452362475392264.jpg b/public/tiktok/7615452362475392264.jpg new file mode 100644 index 0000000..69cfbbc Binary files /dev/null and b/public/tiktok/7615452362475392264.jpg differ diff --git a/public/tiktok/7615454710916533511.jpg b/public/tiktok/7615454710916533511.jpg new file mode 100644 index 0000000..e8cae6e Binary files /dev/null and b/public/tiktok/7615454710916533511.jpg differ diff --git a/public/tiktok/7615791481458117895.jpg b/public/tiktok/7615791481458117895.jpg new file mode 100644 index 0000000..1f274a6 Binary files /dev/null and b/public/tiktok/7615791481458117895.jpg differ diff --git a/public/tiktok/7615792335791787285.jpg b/public/tiktok/7615792335791787285.jpg new file mode 100644 index 0000000..1307359 Binary files /dev/null and b/public/tiktok/7615792335791787285.jpg differ diff --git a/public/tiktok/7615792557251071253.jpg b/public/tiktok/7615792557251071253.jpg new file mode 100644 index 0000000..8636f80 Binary files /dev/null and b/public/tiktok/7615792557251071253.jpg differ diff --git a/public/tiktok/7615792741502487828.jpg b/public/tiktok/7615792741502487828.jpg new file mode 100644 index 0000000..bed899e Binary files /dev/null and b/public/tiktok/7615792741502487828.jpg differ diff --git a/public/tiktok/7615792939800808725.jpg b/public/tiktok/7615792939800808725.jpg new file mode 100644 index 0000000..9aff9cb Binary files /dev/null and b/public/tiktok/7615792939800808725.jpg differ diff --git a/public/tiktok/7615793139785288980.jpg b/public/tiktok/7615793139785288980.jpg new file mode 100644 index 0000000..c9f97fa Binary files /dev/null and b/public/tiktok/7615793139785288980.jpg differ diff --git a/public/tiktok/7615793545781267730.jpg b/public/tiktok/7615793545781267730.jpg new file mode 100644 index 0000000..99ae31a Binary files /dev/null and b/public/tiktok/7615793545781267730.jpg differ diff --git a/public/tiktok/7615795344672132360.jpg b/public/tiktok/7615795344672132360.jpg new file mode 100644 index 0000000..e958351 Binary files /dev/null and b/public/tiktok/7615795344672132360.jpg differ diff --git a/public/tiktok/7615798501636672776.jpg b/public/tiktok/7615798501636672776.jpg new file mode 100644 index 0000000..e4f3d96 Binary files /dev/null and b/public/tiktok/7615798501636672776.jpg differ diff --git a/public/tiktok/7615802426007801096.jpg b/public/tiktok/7615802426007801096.jpg new file mode 100644 index 0000000..0191dc6 Binary files /dev/null and b/public/tiktok/7615802426007801096.jpg differ diff --git a/public/tiktok/7616157825328254215.jpg b/public/tiktok/7616157825328254215.jpg new file mode 100644 index 0000000..ff81a68 Binary files /dev/null and b/public/tiktok/7616157825328254215.jpg differ diff --git a/public/tiktok/7616159717517167879.jpg b/public/tiktok/7616159717517167879.jpg new file mode 100644 index 0000000..4959180 Binary files /dev/null and b/public/tiktok/7616159717517167879.jpg differ diff --git a/public/tiktok/7616163461604609287.jpg b/public/tiktok/7616163461604609287.jpg new file mode 100644 index 0000000..0ee897c Binary files /dev/null and b/public/tiktok/7616163461604609287.jpg differ diff --git a/public/tiktok/7616164443701103893.jpg b/public/tiktok/7616164443701103893.jpg new file mode 100644 index 0000000..01b9b27 Binary files /dev/null and b/public/tiktok/7616164443701103893.jpg differ diff --git a/public/tiktok/7616164636920007957.jpg b/public/tiktok/7616164636920007957.jpg new file mode 100644 index 0000000..ab8f959 Binary files /dev/null and b/public/tiktok/7616164636920007957.jpg differ diff --git a/public/tiktok/7616164791706717461.jpg b/public/tiktok/7616164791706717461.jpg new file mode 100644 index 0000000..09c5857 Binary files /dev/null and b/public/tiktok/7616164791706717461.jpg differ diff --git a/public/tiktok/7616164954739182869.jpg b/public/tiktok/7616164954739182869.jpg new file mode 100644 index 0000000..8bd4c12 Binary files /dev/null and b/public/tiktok/7616164954739182869.jpg differ diff --git a/public/tiktok/7616165126718115093.jpg b/public/tiktok/7616165126718115093.jpg new file mode 100644 index 0000000..c3db034 Binary files /dev/null and b/public/tiktok/7616165126718115093.jpg differ diff --git a/public/tiktok/7616166257783606535.jpg b/public/tiktok/7616166257783606535.jpg new file mode 100644 index 0000000..275424a Binary files /dev/null and b/public/tiktok/7616166257783606535.jpg differ diff --git a/public/tiktok/7616168374267055378.jpg b/public/tiktok/7616168374267055378.jpg new file mode 100644 index 0000000..0510499 Binary files /dev/null and b/public/tiktok/7616168374267055378.jpg differ diff --git a/public/tiktok/7616535620449619220.jpg b/public/tiktok/7616535620449619220.jpg new file mode 100644 index 0000000..645d6bc Binary files /dev/null and b/public/tiktok/7616535620449619220.jpg differ diff --git a/public/tiktok/7616535801245093141.jpg b/public/tiktok/7616535801245093141.jpg new file mode 100644 index 0000000..470b56a Binary files /dev/null and b/public/tiktok/7616535801245093141.jpg differ diff --git a/public/tiktok/7616536083496455445.jpg b/public/tiktok/7616536083496455445.jpg new file mode 100644 index 0000000..2c8a9d3 Binary files /dev/null and b/public/tiktok/7616536083496455445.jpg differ diff --git a/public/tiktok/7616536245102972180.jpg b/public/tiktok/7616536245102972180.jpg new file mode 100644 index 0000000..51a3e1d Binary files /dev/null and b/public/tiktok/7616536245102972180.jpg differ diff --git a/public/tiktok/7616536467678055701.jpg b/public/tiktok/7616536467678055701.jpg new file mode 100644 index 0000000..cccd7e5 Binary files /dev/null and b/public/tiktok/7616536467678055701.jpg differ diff --git a/public/tiktok/7616902084092480789.jpg b/public/tiktok/7616902084092480789.jpg new file mode 100644 index 0000000..ef1eb84 Binary files /dev/null and b/public/tiktok/7616902084092480789.jpg differ diff --git a/public/tiktok/7616902289504193812.jpg b/public/tiktok/7616902289504193812.jpg new file mode 100644 index 0000000..e0b9bf2 Binary files /dev/null and b/public/tiktok/7616902289504193812.jpg differ diff --git a/public/tiktok/7616902503128501524.jpg b/public/tiktok/7616902503128501524.jpg new file mode 100644 index 0000000..ae51cef Binary files /dev/null and b/public/tiktok/7616902503128501524.jpg differ diff --git a/public/tiktok/7616902684678917397.jpg b/public/tiktok/7616902684678917397.jpg new file mode 100644 index 0000000..85b3c30 Binary files /dev/null and b/public/tiktok/7616902684678917397.jpg differ diff --git a/public/tiktok/7616902964158008596.jpg b/public/tiktok/7616902964158008596.jpg new file mode 100644 index 0000000..bdfcec8 Binary files /dev/null and b/public/tiktok/7616902964158008596.jpg differ diff --git a/public/tiktok/7618023226857311508.jpg b/public/tiktok/7618023226857311508.jpg new file mode 100644 index 0000000..592093d Binary files /dev/null and b/public/tiktok/7618023226857311508.jpg differ diff --git a/public/tiktok/7618023404637130005.jpg b/public/tiktok/7618023404637130005.jpg new file mode 100644 index 0000000..258e663 Binary files /dev/null and b/public/tiktok/7618023404637130005.jpg differ diff --git a/public/tiktok/7618023580994948372.jpg b/public/tiktok/7618023580994948372.jpg new file mode 100644 index 0000000..5b28a9a Binary files /dev/null and b/public/tiktok/7618023580994948372.jpg differ diff --git a/public/tiktok/7618023673798151444.jpg b/public/tiktok/7618023673798151444.jpg new file mode 100644 index 0000000..5b28a9a Binary files /dev/null and b/public/tiktok/7618023673798151444.jpg differ diff --git a/public/tiktok/7618023828597312788.jpg b/public/tiktok/7618023828597312788.jpg new file mode 100644 index 0000000..1f92bba Binary files /dev/null and b/public/tiktok/7618023828597312788.jpg differ diff --git a/public/tiktok/7618030206959340818.jpg b/public/tiktok/7618030206959340818.jpg new file mode 100644 index 0000000..b5da91b Binary files /dev/null and b/public/tiktok/7618030206959340818.jpg differ diff --git a/public/tiktok/7618031655797738760.jpg b/public/tiktok/7618031655797738760.jpg new file mode 100644 index 0000000..6461619 Binary files /dev/null and b/public/tiktok/7618031655797738760.jpg differ diff --git a/public/tiktok/7618032920892148999.jpg b/public/tiktok/7618032920892148999.jpg new file mode 100644 index 0000000..e5fa08f Binary files /dev/null and b/public/tiktok/7618032920892148999.jpg differ diff --git a/public/tiktok/7618034303322148114.jpg b/public/tiktok/7618034303322148114.jpg new file mode 100644 index 0000000..7204b29 Binary files /dev/null and b/public/tiktok/7618034303322148114.jpg differ diff --git a/public/tiktok/7618035152551218450.jpg b/public/tiktok/7618035152551218450.jpg new file mode 100644 index 0000000..1b0ea6c Binary files /dev/null and b/public/tiktok/7618035152551218450.jpg differ diff --git a/public/videos/traveler-day-4-miranda.mp4 b/public/videos/traveler-day-4-miranda.mp4 new file mode 100644 index 0000000..1bc302b Binary files /dev/null and b/public/videos/traveler-day-4-miranda.mp4 differ diff --git a/public/videos/traveler-day-4.mp4 b/public/videos/traveler-day-4.mp4 new file mode 100644 index 0000000..b946714 Binary files /dev/null and b/public/videos/traveler-day-4.mp4 differ diff --git a/scripts/create-admin.ts b/scripts/create-admin.ts new file mode 100644 index 0000000..f512789 --- /dev/null +++ b/scripts/create-admin.ts @@ -0,0 +1,61 @@ +/** + * Create or reset an admin user. + * + * npx tsx scripts/create-admin.ts [full_name] + * + * Idempotent — if the email already exists the password_hash is rotated. + * Requires MYSQL_HOST/USER/PASSWORD/DATABASE in env (read /opt/hi2b/.env on prod). + */ +import 'dotenv/config' +import bcrypt from 'bcryptjs' +import mysql from 'mysql2/promise' + +async function main() { + const [email, password, fullName = 'Admin'] = process.argv.slice(2) + if (!email || !password) { + console.error('Usage: npx tsx scripts/create-admin.ts [full_name]') + process.exit(1) + } + + const { MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE } = process.env + if (!MYSQL_HOST || !MYSQL_USER || !MYSQL_PASSWORD || !MYSQL_DATABASE) { + console.error('MYSQL_* env vars missing') + process.exit(1) + } + + const conn = await mysql.createConnection({ + host: MYSQL_HOST, user: MYSQL_USER, password: MYSQL_PASSWORD, database: MYSQL_DATABASE, + }) + + await conn.execute(` + CREATE TABLE IF NOT EXISTS admin_users ( + id INT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + full_name VARCHAR(255), + role VARCHAR(50) DEFAULT 'admin', + status VARCHAR(20) DEFAULT 'active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + `) + + const hash = await bcrypt.hash(password, 12) + const [existing] = await conn.execute('SELECT id FROM admin_users WHERE email = ?', [email]) as any[] + if (existing.length > 0) { + await conn.execute( + 'UPDATE admin_users SET password_hash = ?, full_name = ?, status = ?, role = ? WHERE email = ?', + [hash, fullName, 'active', 'admin', email] + ) + console.log(`Rotated password for existing admin: ${email}`) + } else { + await conn.execute( + 'INSERT INTO admin_users (email, password_hash, full_name, role, status) VALUES (?, ?, ?, ?, ?)', + [email, hash, fullName, 'admin', 'active'] + ) + console.log(`Created admin: ${email}`) + } + await conn.end() +} + +main().catch(err => { console.error(err); process.exit(1) }) diff --git a/scripts/followup-stats.ts b/scripts/followup-stats.ts new file mode 100644 index 0000000..d1b1db3 --- /dev/null +++ b/scripts/followup-stats.ts @@ -0,0 +1,37 @@ +/** Funnel + follow-up drip stats. npx tsx scripts/followup-stats.ts */ +import 'dotenv/config' +import pool from '../src/lib/db-mysql' + +const c = async (sql: string): Promise => { + const [r] = await pool.execute(sql) as any[] + return Number(r[0]?.c ?? 0) +} + +async function main() { + const leads = await c('SELECT COUNT(*) c FROM ebook_leads') + const verified = await c('SELECT COUNT(*) c FROM early_leads WHERE confirmed=1') + const unverified = await c('SELECT COUNT(*) c FROM early_leads WHERE confirmed=0') + const abandoned = await c("SELECT COUNT(*) c FROM signups WHERE payment_status IS NULL OR payment_status NOT IN ('active','completed')") + const customers = await c("SELECT COUNT(*) c FROM signups WHERE payment_status IN ('active','completed')") + const [rev] = await pool.execute("SELECT COALESCE(SUM(amount),0) r FROM signups WHERE payment_status IN ('active','completed')") as any[] + + console.log('\n=== hi2b FUNNEL ===') + console.log(` Leads (emails captured): ${leads}`) + console.log(` verified: ${verified}`) + console.log(` unverified: ${unverified}`) + console.log(` Abandoned checkouts: ${abandoned}`) + console.log(` Paid customers: ${customers}`) + console.log(` Revenue booked: $${Number(rev[0]?.r || 0)}`) + const cr = leads > 0 ? ((customers / leads) * 100).toFixed(1) : '0' + console.log(` Lead -> customer: ${cr}%`) + + console.log('\n=== FOLLOW-UPS SENT ===') + const [fu] = await pool.execute( + 'SELECT segment, step, COUNT(*) c FROM email_followups GROUP BY segment, step ORDER BY segment, step' + ) as any[] + if (!fu.length) console.log(' (none sent yet)') + for (const row of fu) console.log(` ${row.segment} step${row.step + 1}: ${row.c}`) + console.log('') + await pool.end() +} +main().catch(e => { console.error(e); process.exit(1) }) diff --git a/scripts/generate-ebook.ts b/scripts/generate-ebook.ts new file mode 100644 index 0000000..3e21d9c --- /dev/null +++ b/scripts/generate-ebook.ts @@ -0,0 +1,553 @@ +/** + * Generate the Budget Luxury Travel e-book PDF + * Run: npx tsx scripts/generate-ebook.ts + * + * Pricing and branding are kept in one place (CONFIG) so this stays in sync + * with PAYMENT_CONFIG in src/app/lp/_config/types.ts. + */ +import PDFDocument from 'pdfkit' +import { createWriteStream } from 'fs' +import { join } from 'path' + +const outputPath = join(__dirname, '..', 'public', 'ebooks', 'budget-luxury-travel.pdf') + +// ─── SITE CONFIG (keep in sync with PAYMENT_CONFIG) ────── +const CONFIG = { + // Regular (anchor) price + regularMonthlyPrice: 39, + regularTotalPrice: 390, + // Promo / today's price + monthlyPrice: 29, + totalMonths: 10, + totalPrice: 290, + oneTimePrice: 249, + savingsVsRegularTotal: 141, // regularTotalPrice - oneTimePrice + savingsVsRegularMonthly: 100, // regularTotalPrice - totalPrice + savingsOneTime: 41, // totalPrice - oneTimePrice + nights: 4, + days: 5, + bookingMonths: 18, + guaranteeDays: 30, + phone: '888-602-2424', + domain: 'hi2b.com', + siteUrl: 'https://hi2b.com', + primaryLPSlug: 'golden-hour', + brand: 'Mexico Paradise Vacations', + dailyCost: 0.97, // monthlyPrice * 12 / 365 — one year of $29/mo +} + +const doc = new PDFDocument({ + size: 'letter', + // Bottom margin intentionally small so the page-number footer (rendered at + // y = page.height - 50) does not trigger PDFKit's auto-pagination. + margins: { top: 72, bottom: 30, left: 72, right: 72 }, + info: { + Title: `The ${CONFIG.brand} Insider Guide — Mexico for Under $1/Day`, + Author: CONFIG.brand, + Subject: 'Travel Guide', + }, +}) + +const stream = createWriteStream(outputPath) +doc.pipe(stream) + +// Color palette +const NAVY = '#15396B' +const GOLD = '#D4A574' +const DARK = '#1a1a2e' +const GRAY = '#444444' +const LIGHT_GRAY = '#888888' +const WHITE = '#FFFFFF' +const TEAL = '#0d7377' +const ACCENT = '#E8651A' +const GREEN = '#16A34A' + +// ─── COVER PAGE ──────────────────────────────────────────── +doc.rect(0, 0, doc.page.width, doc.page.height).fill(NAVY) + +doc.rect(60, 180, doc.page.width - 120, 2).fill(GOLD) + +doc.font('Helvetica-Bold').fontSize(42).fillColor(WHITE) + .text('Budget Luxury', 72, 220, { align: 'center' }) +doc.text('Travel in Mexico', 72, 275, { align: 'center' }) + +doc.font('Helvetica').fontSize(18).fillColor(GOLD) + .text(`5 Days of All-Inclusive Paradise for Under $${CONFIG.oneTimePrice}`, 72, 345, { align: 'center' }) +doc.text('The Insider Guide', 72, 370, { align: 'center' }) + +doc.rect(60, 420, doc.page.width - 120, 2).fill(GOLD) + +doc.font('Helvetica').fontSize(11).fillColor('#aabbcc') + .text(`A Free Guide from ${CONFIG.brand}`, 72, 460, { align: 'center' }) + +doc.font('Helvetica').fontSize(9).fillColor('#667799') + .text(CONFIG.siteUrl, 72, 680, { align: 'center' }) + .text(`© ${new Date().getFullYear()} ${CONFIG.brand}. All rights reserved.`, 72, 695, { align: 'center' }) + +// ─── TABLE OF CONTENTS ──────────────────────────────────── +doc.addPage() +doc.rect(0, 0, doc.page.width, 100).fill(NAVY) +doc.font('Helvetica-Bold').fontSize(28).fillColor(WHITE) + .text('Table of Contents', 72, 40, { align: 'center' }) + +const tocItems: [string, string, string][] = [ + ['Introduction', 'Why Mexico? Why Now?', '3'], + ['Secret #1', 'Vacation Certificates — The Insider Hack', '4'], + ['Secret #2', 'Shoulder Season = Best Season', '5'], + ['Secret #3', 'All-Inclusive: The Hidden Math', '6'], + ['Secret #4', 'Destination Deep Dives', '7'], + ['Secret #5', 'The $29/mo Payment Plan Advantage', '9'], + ['Bonus', 'Your Mexico Packing Checklist', '10'], + ['Next Steps', 'Ready to Claim Your Paradise?', '11'], +] + +let tocY = 140 +tocItems.forEach(([title, subtitle, page]) => { + doc.font('Helvetica-Bold').fontSize(14).fillColor(NAVY) + .text(title, 72, tocY) + doc.font('Helvetica').fontSize(11).fillColor(GRAY) + .text(subtitle, 72, tocY + 18) + doc.font('Helvetica').fontSize(11).fillColor(LIGHT_GRAY) + .text(page, 480, tocY + 5, { align: 'right', width: 60 }) + doc.font('Helvetica').fontSize(9).fillColor('#cccccc') + const titleWidth = doc.widthOfString(subtitle, { font: 'Helvetica', fontSize: 11 }) + const dotsStart = 80 + titleWidth + const dotsEnd = 475 + if (dotsEnd > dotsStart + 10) { + const dots = '.'.repeat(Math.floor((dotsEnd - dotsStart) / 4)) + doc.text(dots, dotsStart, tocY + 20, { width: dotsEnd - dotsStart }) + } + tocY += 55 +}) + +// ─── Helpers ─────────────────────────────────────────────── +function pageHeader(title: string, subtitle?: string) { + doc.addPage() + doc.rect(0, 0, doc.page.width, 90).fill(NAVY) + doc.font('Helvetica-Bold').fontSize(24).fillColor(WHITE) + .text(title, 72, 30, { width: doc.page.width - 144 }) + if (subtitle) { + doc.font('Helvetica').fontSize(12).fillColor(GOLD) + .text(subtitle, 72, 60, { width: doc.page.width - 144 }) + } + return 120 +} + +function sectionHeading(_y: number, text: string): number { + doc.font('Helvetica-Bold').fontSize(16).fillColor(NAVY) + .text(text, 72, _y, { width: doc.page.width - 144 }) + return doc.y + 10 +} + +function bodyText(_y: number, content: string): number { + doc.font('Helvetica').fontSize(11).fillColor(GRAY) + .text(content, 72, _y, { + width: doc.page.width - 144, + lineGap: 4, + }) + return doc.y + 10 +} + +function bulletPoint(_y: number, content: string): number { + doc.font('Helvetica').fontSize(11).fillColor(TEAL).text('•', 82, _y) + doc.font('Helvetica').fontSize(11).fillColor(GRAY) + .text(content, 100, _y, { width: doc.page.width - 172, lineGap: 3 }) + return doc.y + 4 +} + +function highlightBox(y: number, title: string, content: string, accent = TEAL, bg = '#f0f7ff'): number { + const contentH = doc.heightOfString(content, { width: doc.page.width - 180, fontSize: 10, lineGap: 3 }) + const boxHeight = contentH + 54 + doc.roundedRect(72, y, doc.page.width - 144, boxHeight, 8).fill(bg) + doc.rect(72, y, 4, boxHeight).fill(accent) + doc.font('Helvetica-Bold').fontSize(11).fillColor(accent) + .text(title, 88, y + 12, { width: doc.page.width - 180 }) + doc.font('Helvetica').fontSize(10).fillColor(GRAY) + .text(content, 88, y + 30, { width: doc.page.width - 180, lineGap: 3 }) + return y + boxHeight + 16 +} + +function pageFooter(pageNum: number) { + // Force footer to land on the current page by resetting the text cursor to the + // footer position before writing. Without this, if prior content pushed the + // internal cursor near/past the page bottom, PDFKit auto-breaks to a new page + // even when we pass explicit (x, y) to .text() — resulting in blank spacer pages. + doc.rect(72, doc.page.height - 60, doc.page.width - 144, 0.5).fill('#dddddd') + doc.save() + doc.y = doc.page.height - 50 + doc.x = 72 + doc.font('Helvetica').fontSize(8).fillColor(LIGHT_GRAY) + .text(`${CONFIG.brand} — Page ${pageNum} — ${CONFIG.domain}`, 72, doc.page.height - 50, { + align: 'center', + width: doc.page.width - 144, + lineBreak: false, + }) + doc.restore() +} + +// ─── PAGE 3: INTRODUCTION ───────────────────────────────── +let y = pageHeader('Introduction', 'Why Mexico? Why Now?') + +y = bodyText(y, 'Mexico has been one of the world\'s most beloved vacation destinations for decades — and for good reason. From the powdery white sands of Cancun to the dramatic cliffs of Cabo San Lucas, from the ancient Mayan ruins of the Riviera Maya to the cobblestone charm of Puerto Vallarta, Mexico offers an extraordinary range of experiences that few countries can match.') + +y = bodyText(y, `But here\'s what most travelers don\'t know: you don\'t have to spend $3,000-$5,000 to enjoy a luxury all-inclusive Mexican vacation. Savvy travelers have been enjoying 5-star resort experiences for a fraction of that price — often under $${CONFIG.oneTimePrice} total.`) + +y = bodyText(y, 'In this guide, we\'re pulling back the curtain on the five biggest secrets that budget-conscious luxury travelers use to experience world-class Mexican resorts without the world-class price tag.') + +y = sectionHeading(y + 8, 'What You\'ll Learn') +y = bulletPoint(y, 'How vacation certificates can save you 85-90% on luxury resort stays') +y = bulletPoint(y, 'The best times to travel for incredible weather AND incredible prices') +y = bulletPoint(y, 'Why all-inclusive actually saves you more money than you think') +y = bulletPoint(y, 'Which Mexican destination is perfect for YOUR travel style') +y = bulletPoint(y, `How the $${CONFIG.monthlyPrice}/mo payment plan makes luxury travel accessible to any budget`) + +y = highlightBox(y + 4, 'Did You Know?', `The average American spends $1,979 per person on a domestic vacation. For $${CONFIG.oneTimePrice} total — or just $${CONFIG.monthlyPrice}/month — you and a partner can enjoy a ${CONFIG.days}-day, ${CONFIG.nights}-night all-inclusive Mexico vacation. That\'s over 85% less than the average trip.`) + +pageFooter(3) + +// ─── PAGE 4: SECRET #1 ──────────────────────────────────── +y = pageHeader('Secret #1', 'Vacation Certificates — The Insider Hack') + +y = bodyText(y, 'Here\'s a little-known fact about the resort industry: luxury resorts in Mexico have a powerful incentive to let you stay for almost nothing. It\'s called a "vacation certificate," and it\'s the single most effective way to experience luxury travel on a budget.') + +y = sectionHeading(y, 'How Vacation Certificates Work') +y = bodyText(y, 'Luxury resorts invest millions in their properties — infinity pools, gourmet restaurants, spa facilities, world-class amenities. Their biggest challenge? Getting potential long-term customers through the door.') + +y = bodyText(y, `That\'s where vacation certificates come in. Resorts partner with companies like ${CONFIG.brand} to offer deeply discounted ${CONFIG.days}-day/${CONFIG.nights}-night all-inclusive stays. In exchange, guests attend a brief 90-minute resort tour and presentation about vacation ownership. There is absolutely no obligation to purchase anything.`) + +y = sectionHeading(y, 'The Numbers Speak for Themselves') +y = bulletPoint(y, `Rack rate for ${CONFIG.nights} nights at a luxury all-inclusive: $2,500 - $4,000`) +y = bulletPoint(y, 'Booking through Expedia or Hotels.com: $1,800 - $3,000') +y = bulletPoint(y, `Regular certificate price: $${CONFIG.regularMonthlyPrice}/mo x ${CONFIG.totalMonths} = $${CONFIG.regularTotalPrice}`) +y = bulletPoint(y, `Promo price today: $${CONFIG.monthlyPrice}/mo x ${CONFIG.totalMonths} = $${CONFIG.totalPrice} (save $${CONFIG.savingsVsRegularMonthly})`) +y = bulletPoint(y, `Best value: $${CONFIG.oneTimePrice} one-time (save $${CONFIG.savingsVsRegularTotal} vs regular)`) +y = bulletPoint(y, `Your savings vs retail: $1,510 - $3,751 per trip`) + +y = highlightBox(y + 4, 'Pro Tip', `The regular rate is $${CONFIG.regularMonthlyPrice}/mo, but our current promo drops it to $${CONFIG.monthlyPrice}/mo — under $1 a day. Or pay once at $${CONFIG.oneTimePrice} and save $${CONFIG.savingsVsRegularTotal} versus the regular total. Every certificate is covered by our ${CONFIG.guaranteeDays}-day 100% money-back guarantee, and bringing a guest is always free.`) + +pageFooter(4) + +// ─── PAGE 5: SECRET #2 ──────────────────────────────────── +y = pageHeader('Secret #2', 'Shoulder Season = Best Season') + +y = bodyText(y, 'Most travelers instinctively book during December holidays, spring break, or summer — exactly when prices are at their peak and beaches are packed. Smart travelers know that "shoulder season" offers the perfect sweet spot of great weather, low prices, and uncrowded beaches.') + +y = sectionHeading(y, 'Peak Season vs. Shoulder Season') + +const tableTop = y +const colW = (doc.page.width - 144) / 3 +doc.rect(72, tableTop, doc.page.width - 144, 25).fill(NAVY) +doc.font('Helvetica-Bold').fontSize(10).fillColor(WHITE) +doc.text('', 72, tableTop + 8, { width: colW, align: 'center' }) +doc.text('Peak Season', 72 + colW, tableTop + 8, { width: colW, align: 'center' }) +doc.text('Shoulder Season', 72 + colW * 2, tableTop + 8, { width: colW, align: 'center' }) + +const tableRows = [ + ['Months', 'Dec-Mar, Jun-Aug', 'Apr-May, Sep-Nov'], + ['Weather', '80-90°F, Sunny', '80-88°F, Sunny'], + ['Crowds', 'Very crowded', 'Light crowds'], + ['Prices', '+40-60% premium', '-30-50% savings'], + ['Service', 'Stretched thin', 'Attentive & personal'], + ['Upgrades', 'Rarely available', 'Often complimentary'], +] + +let rowY = tableTop + 25 +tableRows.forEach((row, i) => { + const bg = i % 2 === 0 ? '#f8f9fa' : WHITE + doc.rect(72, rowY, doc.page.width - 144, 22).fill(bg) + doc.font('Helvetica-Bold').fontSize(9).fillColor(DARK) + .text(row[0], 82, rowY + 6, { width: colW - 20 }) + doc.font('Helvetica').fontSize(9).fillColor(GRAY) + .text(row[1], 72 + colW, rowY + 6, { width: colW, align: 'center' }) + doc.font('Helvetica-Bold').fontSize(9).fillColor(TEAL) + .text(row[2], 72 + colW * 2, rowY + 6, { width: colW, align: 'center' }) + rowY += 22 +}) + +y = rowY + 20 +y = bodyText(y, 'The bottom line: you\'ll enjoy nearly identical weather, significantly fewer crowds, better service, and prices 30-50% lower. Many seasoned travelers say shoulder season is actually the BEST time to visit Mexico.') + +y = highlightBox(y, 'Best Shoulder Season Windows', '• April 15 - May 31: Perfect weather, post-spring-break calm\n• September 15 - November 30: Hurricane season winding down, incredible deals\n• Early December (before Dec 15): Holiday decorations without holiday crowds') + +pageFooter(5) + +// ─── PAGE 6: SECRET #3 ──────────────────────────────────── +y = pageHeader('Secret #3', 'All-Inclusive: The Hidden Math') + +y = bodyText(y, 'When most people see "all-inclusive" pricing, they think it\'s more expensive. But when you actually do the math, all-inclusive resorts almost always save you significant money — and eliminate the stress of watching every peso you spend.') + +y = sectionHeading(y, 'The Real Cost of a "Budget" Resort Stay') +y = bodyText(y, 'Let\'s break down what a couple typically spends per day at a non-all-inclusive resort in Mexico:') + +const expenses = [ + ['Breakfast', '$15-25/person', '$30-50'], + ['Lunch', '$20-35/person', '$40-70'], + ['Dinner', '$40-80/person', '$80-160'], + ['Drinks (pool/beach)', '$8-15 each × 4', '$32-60'], + ['Evening drinks', '$10-18 each × 3', '$30-54'], + ['Pool/beach chairs', '', '$20-40'], + ['Tips', '', '$25-40'], + ['Activities', '', '$40-100'], +] + +let expY = y +doc.rect(72, expY, doc.page.width - 144, 22).fill(NAVY) +doc.font('Helvetica-Bold').fontSize(9).fillColor(WHITE) +doc.text('Expense', 82, expY + 7) +doc.text('Per Person', 280, expY + 7) +doc.text('Couple Total', 420, expY + 7) +expY += 22 + +expenses.forEach((row, i) => { + const bg = i % 2 === 0 ? '#fef9f0' : WHITE + doc.rect(72, expY, doc.page.width - 144, 18).fill(bg) + doc.font('Helvetica').fontSize(9).fillColor(GRAY) + doc.text(row[0], 82, expY + 5) + doc.text(row[1], 280, expY + 5) + doc.font('Helvetica-Bold').fontSize(9).fillColor(ACCENT) + doc.text(row[2], 420, expY + 5) + expY += 18 +}) + +doc.rect(72, expY, doc.page.width - 144, 22).fill('#fff3e0') +doc.font('Helvetica-Bold').fontSize(10).fillColor(ACCENT) + .text('DAILY TOTAL (couple):', 82, expY + 6) + .text('$297 - $574', 420, expY + 6) +expY += 30 + +y = expY + 8 +y = bodyText(y, `Over a ${CONFIG.days}-day trip, that\'s $1,485 - $2,870 in additional costs on top of your room rate. With an all-inclusive certificate at $${CONFIG.oneTimePrice}, ALL of this is included — food, drinks, pools, beaches, resort amenities.`) + +y = highlightBox(y, 'The Bottom Line', `A "cheap" hotel room + food & drinks costs $2,000-$4,000 for a couple.\nAn all-inclusive vacation certificate: just $${CONFIG.oneTimePrice} — or $${CONFIG.monthlyPrice}/mo for ${CONFIG.totalMonths} months.\nYour savings: $1,750 - $3,750. It\'s not even close.`, ACCENT, '#fff3e0') + +pageFooter(6) + +// ─── PAGE 7: SECRET #4 ───────────────────────────────── +y = pageHeader('Secret #4', 'Destination Deep Dives — Find Your Perfect Match') + +y = bodyText(y, 'Not all Mexican destinations are created equal — and choosing the right one for your travel style can make the difference between a good vacation and an unforgettable one. Here\'s our insider guide to the four premier destinations your certificate covers.') + +y = sectionHeading(y, 'Cancun') +y = bodyText(y, 'Best for: Beach lovers, nightlife enthusiasts, and first-time Mexico visitors.') +y = bodyText(y, 'Cancun\'s Hotel Zone is a 14-mile strip of powder-white beach backed by turquoise Caribbean waters. It\'s the most popular tourist destination in Mexico for good reason — stunning beaches, world-class resorts, and easy access from most US cities with direct flights under 3 hours.') + +y = bulletPoint(y, 'Must-Do: Take the ferry to Isla Mujeres for a laid-back island day') +y = bulletPoint(y, 'Must-Do: Snorkel the underwater museum MUSA (500+ submerged sculptures)') +y = bulletPoint(y, 'Must-Do: Visit Chichen Itza — one of the New Seven Wonders of the World') +y = bulletPoint(y, 'Best For: Couples seeking beach + nightlife, families with older kids') + +y += 8 +y = sectionHeading(y, 'Cabo San Lucas') +y = bodyText(y, 'Best for: Dramatic scenery, deep-sea fishing, and luxury seekers.') +y = bodyText(y, 'Where the Pacific Ocean meets the Sea of Cortez, Cabo offers some of Mexico\'s most dramatic landscapes. The iconic El Arco rock formation, desert-meets-ocean terrain, and some of the finest resorts in the world make Cabo a favorite of celebrities and luxury travelers.') + +y = bulletPoint(y, 'Must-Do: See El Arco at sunset by boat or kayak') +y = bulletPoint(y, 'Must-Do: Whale watching (December - April) — grey and humpback whales') +y = bulletPoint(y, 'Must-Do: Take a desert ATV tour through the Baja landscape') +y = bulletPoint(y, 'Best For: Couples, golf enthusiasts, deep-sea fishing fans') + +pageFooter(7) + +// ─── PAGE 8: SECRET #4 (continued) ───────────────────────── +y = pageHeader('Secret #4 (continued)', 'More Dream Destinations') + +y = sectionHeading(y, 'Riviera Maya') +y = bodyText(y, 'Best for: Adventure seekers, culture lovers, and eco-tourists.') +y = bodyText(y, 'Stretching along the Caribbean coast south of Cancun, the Riviera Maya combines stunning beaches with ancient Mayan heritage. This is where you\'ll find cenotes (natural swimming holes), jungle adventures, and the cliff-top ruins of Tulum overlooking the turquoise sea.') + +y = bulletPoint(y, 'Must-Do: Swim in a cenote — there are over 6,000 in the Yucatan Peninsula') +y = bulletPoint(y, 'Must-Do: Visit Tulum ruins at sunrise before the crowds arrive') +y = bulletPoint(y, 'Must-Do: Snorkel with sea turtles in Akumal Bay') +y = bulletPoint(y, 'Best For: Adventure couples, eco-travelers, history buffs') + +y += 8 +y = sectionHeading(y, 'Puerto Vallarta') +y = bodyText(y, 'Best for: Culture enthusiasts, foodies, and sunset chasers.') +y = bodyText(y, 'Puerto Vallarta is Mexico\'s most authentic resort city. Unlike purpose-built resort zones, PV has a real downtown with cobblestone streets, local markets, world-class restaurants, and a legendary Malecón (boardwalk) that comes alive every evening with street performers and art.') + +y = bulletPoint(y, 'Must-Do: Walk the Malecón at sunset — the most beautiful boardwalk in Mexico') +y = bulletPoint(y, 'Must-Do: Take a food tour through the Romantic Zone') +y = bulletPoint(y, 'Must-Do: Day trip to the hidden beach town of Sayulita') +y = bulletPoint(y, 'Best For: Foodies, couples seeking authenticity, culture lovers') + +y += 12 +const quizBoxH = 90 +doc.roundedRect(72, y, doc.page.width - 144, quizBoxH, 8).fill('#e8f5e9') +doc.rect(72, y, 4, quizBoxH).fill(TEAL) +doc.font('Helvetica-Bold').fontSize(12).fillColor(TEAL) + .text('Not Sure Which Destination? Quick Guide:', 88, y + 12) +doc.font('Helvetica').fontSize(10).fillColor(GRAY) + .text('Want the best beaches? → Cancun', 88, y + 32) + .text('Want dramatic luxury? → Cabo San Lucas', 88, y + 47) + .text('Want adventure + culture? → Riviera Maya', 88, y + 62) + .text('Want authentic Mexico? → Puerto Vallarta', 88, y + 77) + +pageFooter(8) + +// ─── PAGE 9: SECRET #5 ──────────────────────────────────── +y = pageHeader('Secret #5', `The $${CONFIG.monthlyPrice}/mo Payment Plan Advantage`) + +y = bodyText(y, 'The biggest psychological barrier to booking a vacation isn\'t the total cost — it\'s the upfront cost. Dropping $2,000+ in one transaction feels painful, even when you can afford it. Smart travelers use payment plans to eliminate this friction entirely.') + +y = sectionHeading(y, 'The Under-$1-a-Day Vacation') +y = bodyText(y, `At ${CONFIG.brand}, you can lock in your all-inclusive vacation certificate for just $${CONFIG.monthlyPrice}/month over ${CONFIG.totalMonths} months. Let\'s put that in perspective:`) + +const comparisons: Array<[string, string, string]> = [ + [`Your daily ${CONFIG.brand} cost`, `$${CONFIG.dailyCost.toFixed(2)}/day`, TEAL], + ['Pack of gum', '$1.50/day', GRAY], + ['Streaming subscription', '$1.80/day', GRAY], + ['Starbucks coffee', '$5.50/day', GRAY], + ['Fast food lunch', '$9.00/day', GRAY], + ['Daily takeout dinner', '$18.00/day', GRAY], +] + +let compY = y +comparisons.forEach(([label, cost, color]) => { + const amount = parseFloat(cost.replace(/[$\/day]/g, '')) + const barWidth = Math.min(amount * 25, 280) + doc.roundedRect(200, compY, barWidth, 18, 4).fill(color === TEAL ? '#e0f2f1' : '#f5f5f5') + doc.roundedRect(200, compY, barWidth, 18, 4).fill(color === TEAL ? TEAL : '#ccc') + .fillOpacity(color === TEAL ? 0.2 : 0.15) + doc.fillOpacity(1) + doc.font('Helvetica').fontSize(9).fillColor(GRAY) + .text(label, 82, compY + 4) + doc.font('Helvetica-Bold').fontSize(9).fillColor(color) + .text(cost, 200 + barWidth + 8, compY + 4) + compY += 24 +}) + +y = compY + 12 +y = sectionHeading(y, 'Why Payment Plans Are Smart') +y = bulletPoint(y, 'Lock in today\'s price before rates increase') +y = bulletPoint(y, 'No credit check required — available to everyone') +y = bulletPoint(y, `Book your travel dates immediately after your first $${CONFIG.monthlyPrice} payment`) +y = bulletPoint(y, `No interest charges — promo price $${CONFIG.monthlyPrice} x ${CONFIG.totalMonths} = $${CONFIG.totalPrice} total (vs regular $${CONFIG.regularTotalPrice})`) +y = bulletPoint(y, `Or save $${CONFIG.savingsVsRegularTotal} with a one-time payment of $${CONFIG.oneTimePrice}`) +y = bulletPoint(y, `${CONFIG.guaranteeDays}-day 100% money-back guarantee — cancel anytime in the first month for a full refund`) + +y = highlightBox(y + 4, 'Important Note', `Regular price is $${CONFIG.regularMonthlyPrice}/mo — you are locking in the $${CONFIG.monthlyPrice}/mo promo. Your certificate is activated after your first $${CONFIG.monthlyPrice} payment, so you can begin booking travel dates immediately. You have ${CONFIG.bookingMonths} months to travel, and bringing a guest is always free.`) + +pageFooter(9) + +// ─── PAGE 10: PACKING CHECKLIST ─────────────────────────── +y = pageHeader('Bonus', 'Your Complete Mexico Packing Checklist') + +const checklistSections = [ + { + title: 'Essential Documents', + items: ['Valid passport (6+ months before expiry)', 'Travel insurance documents', 'Vacation certificate confirmation', 'Digital + paper copies of all documents', 'Hotel/resort confirmation email'], + }, + { + title: 'Clothing', + items: ['Swimsuits (2-3)', 'Light cover-ups and sundresses', 'One nice outfit for resort dining', 'Comfortable walking shoes', 'Flip flops / sandals', 'Light layers for air-conditioned spaces'], + }, + { + title: 'Sun & Health', + items: ['Reef-safe sunscreen SPF 50+ (required at many resorts)', 'UV-protection sunglasses', 'Wide-brim hat', 'Insect repellent with DEET', 'Basic medications & first aid', 'Prescription medications in original bottles'], + }, + { + title: 'Tech & Extras', + items: ['Waterproof phone case', 'Portable battery charger', 'Camera / GoPro', 'Universal power adapter (Mexico uses US plugs, but just in case)', 'Dry bag for beach/boat excursions'], + }, +] + +checklistSections.forEach((section) => { + doc.font('Helvetica-Bold').fontSize(13).fillColor(NAVY) + .text(section.title, 72, y) + y += 20 + section.items.forEach((item) => { + doc.font('Helvetica').fontSize(10).fillColor(GRAY) + doc.rect(82, y + 2, 10, 10).lineWidth(0.5).stroke('#aaaaaa') + doc.text(item, 100, y + 1, { width: doc.page.width - 172 }) + y += 18 + }) + y += 8 +}) + +pageFooter(10) + +// ─── PAGE 11: CTA / CLOSING ────────────────────────────── +doc.addPage() +doc.rect(0, 0, doc.page.width, doc.page.height).fill(NAVY) + +doc.font('Helvetica-Bold').fontSize(32).fillColor(WHITE) + .text('Ready to Claim', 72, 100, { align: 'center' }) + .text('Your Paradise?', 72, 145, { align: 'center' }) + +doc.rect(200, 195, doc.page.width - 400, 2).fill(GOLD) + +doc.font('Helvetica').fontSize(14).fillColor('#aabbcc') + .text('You now have all 5 secrets to budget luxury travel in Mexico.', 72, 225, { align: 'center' }) + .text('Here\'s the quick recap:', 72, 245, { align: 'center' }) + +const recapItems = [ + '1. Use vacation certificates for 85-90% savings', + '2. Travel shoulder season for best weather & prices', + '3. Choose all-inclusive to eliminate hidden costs', + '4. Pick the right destination for your style', + `5. Use the $${CONFIG.monthlyPrice}/mo payment plan to make it effortless`, +] + +let recapY = 290 +recapItems.forEach((item) => { + doc.font('Helvetica').fontSize(13).fillColor(GOLD) + .text(item, 100, recapY, { align: 'center', width: doc.page.width - 200 }) + recapY += 28 +}) + +// Offer box +const offerY = 445 +doc.roundedRect(100, offerY, doc.page.width - 200, 175, 12) + .lineWidth(2).stroke(GOLD) + +doc.font('Helvetica-Bold').fontSize(16).fillColor(WHITE) + .text('YOUR SPECIAL PROMO', 100, offerY + 16, { align: 'center', width: doc.page.width - 200 }) + +doc.font('Helvetica').fontSize(13).fillColor('#aabbcc') + .text(`${CONFIG.days} Days / ${CONFIG.nights} Nights All-Inclusive Mexico Vacation`, 100, offerY + 45, { align: 'center', width: doc.page.width - 200 }) + +// Struck-through regular price +const regularText = `Regular price: $${CONFIG.regularMonthlyPrice}/month` +const regularWidth = doc.widthOfString(regularText, { font: 'Helvetica', fontSize: 12 }) +const regularX = 100 + ((doc.page.width - 200) - regularWidth) / 2 +doc.font('Helvetica').fontSize(12).fillColor('#8899aa') + .text(regularText, regularX, offerY + 70) +// Strikethrough line +doc.rect(regularX, offerY + 78, regularWidth, 1).fill('#8899aa') + +// Today's promo price — big and gold +doc.font('Helvetica-Bold').fontSize(26).fillColor(GOLD) + .text(`Today: $${CONFIG.monthlyPrice}/month for ${CONFIG.totalMonths} months`, 100, offerY + 88, { align: 'center', width: doc.page.width - 200 }) + +doc.font('Helvetica').fontSize(11).fillColor('#aabbcc') + .text(`or save $${CONFIG.savingsVsRegularTotal} with a one-time payment of $${CONFIG.oneTimePrice}`, 100, offerY + 122, { align: 'center', width: doc.page.width - 200 }) + +doc.font('Helvetica-Bold').fontSize(10).fillColor(GREEN) + .text(`${CONFIG.guaranteeDays}-Day Money-Back • Bring a Guest Free • ${CONFIG.bookingMonths}-Month Booking Window`, 100, offerY + 146, { align: 'center', width: doc.page.width - 200 }) + +// Book now CTA — emphasize hi2b.com +doc.font('Helvetica-Bold').fontSize(18).fillColor(WHITE) + .text(`Visit ${CONFIG.domain}`, 72, 645, { align: 'center', width: doc.page.width - 144 }) + +doc.font('Helvetica-Bold').fontSize(16).fillColor(GOLD) + .text(`Or call ${CONFIG.phone}`, 72, 674, { align: 'center', width: doc.page.width - 144 }) + +doc.font('Helvetica').fontSize(9).fillColor('#aabbcc') + .text('Mon-Fri 9am-8pm | Sat 10am-4pm EST | Toll-Free', 72, 698, { align: 'center', width: doc.page.width - 144 }) + +doc.font('Helvetica').fontSize(8).fillColor('#667799') + .text(`© ${new Date().getFullYear()} ${CONFIG.brand}. All rights reserved.`, 72, 720, { align: 'center', width: doc.page.width - 144 }) + .text('This guide is informational. Offers subject to availability and resort terms. ' + + 'Certificate requires attendance at a 90-minute resort tour with no obligation to purchase.', + 72, 734, { align: 'center', width: doc.page.width - 144 }) + +// Finalize +doc.end() + +stream.on('finish', () => { + console.log(`✅ PDF generated: ${outputPath}`) + const fs = require('fs') + const stats = fs.statSync(outputPath) + console.log(` Size: ${(stats.size / 1024).toFixed(1)} KB`) + console.log(` Pages: 11`) +}) diff --git a/scripts/preview-followups.ts b/scripts/preview-followups.ts new file mode 100644 index 0000000..3bda60c --- /dev/null +++ b/scripts/preview-followups.ts @@ -0,0 +1,24 @@ +/** Send every follow-up template to one inbox so you can review them. + * npx tsx scripts/preview-followups.ts [email] (default nick@724care.net) + */ +import 'dotenv/config' +import { sendLeadFollowup, sendAbandonedFollowup, sendBookingReminder } from '../src/lib/email' + +const to = process.argv[2] || 'nick@724care.net' + +async function main() { + const jobs: [string, () => Promise][] = [ + ['lead follow-up 1 (deal waiting)', () => sendLeadFollowup(to, 0)], + ['lead follow-up 2 (offer + call)', () => sendLeadFollowup(to, 1)], + ['lead follow-up 3 (last call)', () => sendLeadFollowup(to, 2)], + ['abandoned 1 (finish booking)', () => sendAbandonedFollowup(to, 0)], + ['abandoned 2 (reassurance + call)', () => sendAbandonedFollowup(to, 1)], + ['customer (booking reminder)', () => sendBookingReminder(to, 'Nick')], + ] + console.log(`Sending ${jobs.length} sample emails to ${to}\n`) + for (const [name, fn] of jobs) { + try { const r = await fn(); console.log(` ${r ? 'SENT ' : 'FAIL '} ${name}`) } + catch (e) { console.log(` ERR ${name}: ${e instanceof Error ? e.message : e}`) } + } +} +main().catch(e => { console.error(e); process.exit(1) }) diff --git a/scripts/scrape-tiktok.ts b/scripts/scrape-tiktok.ts new file mode 100644 index 0000000..f22bd21 --- /dev/null +++ b/scripts/scrape-tiktok.ts @@ -0,0 +1,92 @@ +import puppeteer from 'puppeteer-extra' +import StealthPlugin from 'puppeteer-extra-plugin-stealth' + +puppeteer.use(StealthPlugin()) + +const USERNAME = 'travel.to.mexico8' +const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)) + +async function scrape() { + console.log('Launching stealth browser...') + + const browser = await puppeteer.launch({ + headless: true, + executablePath: '/usr/bin/google-chrome-stable', + args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu'], + }) + + try { + const page = await browser.newPage() + await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36') + await page.setViewport({ width: 1920, height: 1080 }) + + const profileUrl = `https://www.tiktok.com/@${USERNAME}` + console.log(`Navigating to ${profileUrl}...`) + await page.goto(profileUrl, { waitUntil: 'networkidle0', timeout: 45000 }) + await sleep(5000) + + // Scroll aggressively to trigger lazy loading + console.log('Scrolling to load all videos...') + for (let i = 0; i < 15; i++) { + await page.evaluate(() => window.scrollBy(0, 2000)) + await sleep(1000) + } + + // Scroll back up and down again + await page.evaluate(() => window.scrollTo(0, 0)) + await sleep(1000) + for (let i = 0; i < 10; i++) { + await page.evaluate(() => window.scrollBy(0, 1500)) + await sleep(800) + } + + // Extract from page HTML source - TikTok embeds video IDs in the SSR data + const html = await page.content() + + // Method 1: Find video IDs in href attributes + const hrefMatches = html.match(/\/@[^/]+\/video\/(\d+)/g) || [] + const ids = new Set() + hrefMatches.forEach(m => { + const id = m.match(/\/video\/(\d+)/)?.[1] + if (id) ids.add(id) + }) + + // Method 2: Find in JSON data / script tags + const jsonMatches = html.match(/"video\/(\d{15,25})"/g) || [] + jsonMatches.forEach(m => { + const id = m.match(/(\d{15,25})/)?.[1] + if (id) ids.add(id) + }) + + // Method 3: Check __UNIVERSAL_DATA_FOR_REHYDRATION__ or SIGI_STATE + const dataMatches = html.match(/videoId['":\s]+['"](\d{15,25})['"]/g) || [] + dataMatches.forEach(m => { + const id = m.match(/(\d{15,25})/)?.[1] + if (id) ids.add(id) + }) + + // Method 4: Look for video data in script tags + const scriptMatches = html.match(/"id"\s*:\s*"(\d{18,20})"/g) || [] + scriptMatches.forEach(m => { + const id = m.match(/(\d{18,20})/)?.[1] + if (id) ids.add(id) + }) + + const videoIds = Array.from(ids) + console.log(`\nFound ${videoIds.length} video IDs for @${USERNAME}:`) + videoIds.forEach((id, i) => { + console.log(` ${i + 1}. ${id} → https://www.tiktok.com/@${USERNAME}/video/${id}`) + }) + + console.log(`\n--- ARRAY FOR TikTokCarousel.tsx ---`) + console.log(`const TIKTOK_VIDEOS = ${JSON.stringify(videoIds, null, 2)}`) + console.log(`const TIKTOK_USERNAME = '${USERNAME}'`) + + } catch (error) { + console.error('Scrape error:', error) + } finally { + await browser.close() + } +} + +scrape() diff --git a/scripts/send-followups.ts b/scripts/send-followups.ts new file mode 100644 index 0000000..40d900a --- /dev/null +++ b/scripts/send-followups.ts @@ -0,0 +1,125 @@ +/** + * Email follow-up drip worker. Run hourly via cron. + * npx tsx scripts/send-followups.ts # live + * npx tsx scripts/send-followups.ts --dry-run # preview, sends nothing + * + * Segments (mutually exclusive): + * lead — email captured, never started checkout + * abandoned — started checkout, payment not completed + * customer — paid (active/completed) + * + * Dedup is enforced by the email_followups table (unique on email+segment+step), + * so re-running is safe and each recipient advances one step per run. + */ +import 'dotenv/config' +import pool from '../src/lib/db-mysql' +import { sendLeadFollowup, sendAbandonedFollowup, sendBookingReminder } from '../src/lib/email' + +const DRY = process.argv.includes('--dry-run') + +// Internal/test domains to skip (don't drip our own test accounts). +const SUPPRESS_DOMAINS = ['724c.com'] +const suppressed = (email: string) => SUPPRESS_DOMAINS.some(d => email.toLowerCase().endsWith('@' + d)) + +const LEAD_DELAYS = [1, 24, 72] // hours -> steps 0,1,2 +const ABANDONED_DELAYS = [1, 24] // hours -> steps 0,1 +const CUSTOMER_DELAY = 72 // single booking reminder + +async function ensureTable() { + await pool.execute(` + CREATE TABLE IF NOT EXISTS email_followups ( + id INT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) NOT NULL, + segment VARCHAR(32) NOT NULL, + step INT NOT NULL, + sent_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uniq_send (email, segment, step), + INDEX idx_email (email) + )`) +} + +async function sentSteps(segment: string): Promise>> { + const [rows] = await pool.execute('SELECT email, step FROM email_followups WHERE segment=?', [segment]) as any[] + const m = new Map>() + for (const r of rows) { + if (!m.has(r.email)) m.set(r.email, new Set()) + m.get(r.email)!.add(r.step) + } + return m +} + +async function record(email: string, segment: string, step: number) { + await pool.execute('INSERT IGNORE INTO email_followups (email, segment, step) VALUES (?,?,?)', [email, segment, step]) +} + +const hoursSince = (d: string | Date) => (Date.now() - new Date(d).getTime()) / 3.6e6 + +// smallest step index whose delay has elapsed and hasn't been sent yet +function nextDue(delays: number[], age: number, sent: Set): number { + for (let i = 0; i < delays.length; i++) if (!sent.has(i) && age >= delays[i]) return i + return -1 +} + +async function processLeads() { + const [leads] = await pool.execute(` + SELECT e.email, e.created_at + FROM ebook_leads e + LEFT JOIN signups s ON s.email = e.email + WHERE s.id IS NULL + `) as any[] + const sent = await sentSteps('lead') + let n = 0 + for (const l of leads) { + if (suppressed(l.email)) continue + const step = nextDue(LEAD_DELAYS, hoursSince(l.created_at), sent.get(l.email) || new Set()) + if (step < 0) continue + if (!DRY) { await sendLeadFollowup(l.email, step); await record(l.email, 'lead', step) } + console.log(` lead step${step + 1} -> ${l.email}${DRY ? ' (dry)' : ''}`); n++ + } + return n +} + +async function processAbandoned() { + const [rows] = await pool.execute(` + SELECT email, created_at FROM signups + WHERE payment_status IS NULL OR payment_status NOT IN ('active','completed') + `) as any[] + const sent = await sentSteps('abandoned') + let n = 0 + for (const r of rows) { + if (suppressed(r.email)) continue + const step = nextDue(ABANDONED_DELAYS, hoursSince(r.created_at), sent.get(r.email) || new Set()) + if (step < 0) continue + if (!DRY) { await sendAbandonedFollowup(r.email, step); await record(r.email, 'abandoned', step) } + console.log(` abandoned step${step + 1} -> ${r.email}${DRY ? ' (dry)' : ''}`); n++ + } + return n +} + +async function processCustomers() { + const [rows] = await pool.execute(` + SELECT email, full_name, created_at FROM signups + WHERE payment_status IN ('active','completed') + `) as any[] + const sent = await sentSteps('customer') + let n = 0 + for (const r of rows) { + if (suppressed(r.email)) continue + if ((sent.get(r.email) || new Set()).has(0) || hoursSince(r.created_at) < CUSTOMER_DELAY) continue + if (!DRY) { await sendBookingReminder(r.email, r.full_name); await record(r.email, 'customer', 0) } + console.log(` customer reminder -> ${r.email}${DRY ? ' (dry)' : ''}`); n++ + } + return n +} + +async function main() { + await ensureTable() + console.log(`[followups] ${DRY ? 'DRY RUN — ' : ''}scanning…`) + const l = await processLeads() + const a = await processAbandoned() + const c = await processCustomers() + console.log(`[followups] ${l + a + c} email(s): ${l} lead, ${a} abandoned, ${c} customer${DRY ? ' (DRY RUN — nothing sent)' : ''}`) + await pool.end() +} + +main().catch(e => { console.error('followups error:', e); process.exit(1) }) diff --git a/server.ts b/server.ts index 0865ae9..6c2eb0c 100644 --- a/server.ts +++ b/server.ts @@ -5,7 +5,7 @@ import { Server } from 'socket.io'; import next from 'next'; const dev = process.env.NODE_ENV !== 'production'; -const currentPort = 3000; +const currentPort = parseInt(process.env.PORT || '3000', 10); const hostname = '127.0.0.1'; // Custom server with Socket.IO integration diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx new file mode 100644 index 0000000..d0c8b64 --- /dev/null +++ b/src/app/about/page.tsx @@ -0,0 +1,96 @@ +import { Plane, Heart, Shield, Sparkles } from 'lucide-react' +import Link from 'next/link' +import type { Metadata } from 'next' +import { PAYMENT_CONFIG } from '@/app/lp/_config/types' + +export const metadata: Metadata = { + title: 'About Us — Mexico Paradise Vacations', + description: 'How Mexico Paradise Vacations works, who we are, and why our all-inclusive Mexico certificates cost less than direct booking.', +} + +export default function AboutPage() { + return ( +
+
+
+ +
+ +
+ Mexico Paradise Vacations + + Back to Home +
+
+ +
+

About Mexico Paradise Vacations

+

Helping families take the Mexico trip they keep putting off.

+ +
+
+

What we do

+

Mexico Paradise Vacations sells pre-paid all-inclusive vacation certificates to luxury beachfront resorts in Cancun, Cabo, Riviera Maya, and Puerto Vallarta. Each certificate covers 5 days and 4 nights for two adults, with kids under 12 staying free. Once you pay, the certificate is yours to redeem for any available dates in the next 18 months.

+
+ +
+

Why it's so much cheaper than booking direct

+

This is the question almost everyone asks. The honest answer:

+

Resorts don't make their money on the room — they make it on the food, drinks, spa, excursions, and bar tabs you run while you're there. An empty room earns them nothing. So they partner with companies like ours to fill rooms in advance at deeply discounted certificate prices, knowing the rest of your spend will more than cover the gap. It's the same logic behind off-season cruise pricing and last-minute hotel apps — just structured as a pre-paid certificate so the resort can plan inventory.

+

It's real, it's legal, and the resorts you stay at are the same five-star properties listed on every travel site at full price.

+
+ +
+

What you actually pay

+
+

Two payment options:

+
    +
  • ${PAYMENT_CONFIG.monthlyPrice}/month for {PAYMENT_CONFIG.totalMonths} months (total ${PAYMENT_CONFIG.totalPrice})
  • +
  • ${PAYMENT_CONFIG.oneTimePrice} one-time (save ${PAYMENT_CONFIG.totalPrice - PAYMENT_CONFIG.oneTimePrice} vs the monthly plan)
  • +
+

Both options include the full 5-day / 4-night all-inclusive stay for two adults plus kids under 12.

+
+

You can book your travel dates immediately after the first payment. You don't have to wait to finish the payment plan.

+
+ +
+

The money-back guarantee

+

If for any reason you're not satisfied within 30 days of purchase, we'll refund you in full — no questions asked. We can offer this because, frankly, almost nobody asks for it. The math just works.

+
+ +
+

Who we are

+

We're a small US-based team that's been in the travel-certificate business for years. We partner directly with resort groups and pass the savings to families who don't want to overpay for a vacation they deserve. We're not a marketplace, not a points scheme, and not a timeshare — just a pre-paid certificate for a real trip.

+
+ +
+

Where to go from here

+
    +
  • +
    FAQ
    +
    What's included, how to book, refund details.
    +
  • +
  • +
    Real reviews
    +
    What real travelers say about their trips.
    +
  • +
  • +
    Claim a certificate
    +
    5 days, 4 nights, all-inclusive — from ${PAYMENT_CONFIG.monthlyPrice}/mo.
    +
  • +
  • +
    Privacy & Terms
    +
    Privacy Policy · Terms of Service
    +
  • +
+
+ +
+

Contact

+

Questions? Reach out at support@hi2b.com or call 888-602-2424.

+
+
+
+
+ ) +} diff --git a/src/app/admin/affiliates/page.tsx b/src/app/admin/affiliates/page.tsx new file mode 100644 index 0000000..2015178 --- /dev/null +++ b/src/app/admin/affiliates/page.tsx @@ -0,0 +1,185 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Loader2 } from "lucide-react"; + +interface Affiliate { + id: string | number; + name: string; + email: string; + code: string; + commission_rate: number; + status: string; + sales_count: number; + total_earned: number; + total_paid: number; +} + +function affiliateStatusBadge(status: string) { + const s = status?.toLowerCase(); + const styles: Record = { + active: "bg-green-100 text-green-800 border-green-200", + pending: "bg-yellow-100 text-yellow-800 border-yellow-200", + suspended: "bg-red-100 text-red-800 border-red-200", + }; + return ( + + {status} + + ); +} + +export default function AffiliatesPage() { + const [affiliates, setAffiliates] = useState([]); + const [loading, setLoading] = useState(true); + const [actionLoading, setActionLoading] = useState(null); + + async function fetchAffiliates() { + setLoading(true); + try { + const res = await fetch("/api/admin/affiliates"); + if (res.ok) { + const json = await res.json(); + setAffiliates(json.affiliates || []); + } + } catch (err) { + console.error("Failed to fetch affiliates:", err); + } finally { + setLoading(false); + } + } + + useEffect(() => { + fetchAffiliates(); + }, []); + + async function updateStatus(id: string | number, status: string) { + setActionLoading(id); + try { + const res = await fetch("/api/admin/affiliates", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id, status }), + }); + if (res.ok) { + await fetchAffiliates(); + } + } catch (err) { + console.error("Failed to update affiliate:", err); + } finally { + setActionLoading(null); + } + } + + function getActions(affiliate: Affiliate) { + const s = affiliate.status?.toLowerCase(); + const isLoading = actionLoading === affiliate.id; + + if (s === "pending") { + return ( + + ); + } + if (s === "active") { + return ( + + ); + } + if (s === "suspended") { + return ( + + ); + } + return null; + } + + return ( +
+ + + Affiliates + + +
+ + + + + + + + + + + + + + + + {loading ? ( + Array.from({ length: 5 }).map((_, i) => ( + + {Array.from({ length: 9 }).map((_, j) => ( + + ))} + + )) + ) : affiliates.length > 0 ? ( + affiliates.map((aff) => ( + + + + + + + + + + + + )) + ) : ( + + + + )} + +
NameEmailCodeCommissionStatusSalesTotal EarnedTotal PaidActions
+
+
{aff.name}{aff.email}{aff.code}{(aff.commission_rate * 100).toFixed(0)}%{affiliateStatusBadge(aff.status)}{aff.sales_count} + ${Number(aff.total_earned).toLocaleString("en-US", { minimumFractionDigits: 2 })} + + ${Number(aff.total_paid).toLocaleString("en-US", { minimumFractionDigits: 2 })} + {getActions(aff)}
+ No affiliates found +
+
+
+
+
+ ); +} diff --git a/src/app/admin/analytics/page.tsx b/src/app/admin/analytics/page.tsx new file mode 100644 index 0000000..66630a4 --- /dev/null +++ b/src/app/admin/analytics/page.tsx @@ -0,0 +1,265 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { + ResponsiveContainer, + AreaChart, + Area, + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + CartesianGrid, + PieChart, + Pie, + Cell, +} from "recharts"; + +interface AnalyticsData { + salesByLP: { name: string; count: number }[]; + salesBySource: { name: string; count: number }[]; + salesByAffiliate: { name: string; count: number }[]; + revenueOverTime: { date: string; revenue: number }[]; + funnel: { + pageViews: number; + ebookDownloads: number; + signups: number; + paidCustomers: number; + }; +} + +const PIE_COLORS = ["#ea580c", "#f97316", "#fb923c", "#fdba74", "#fed7aa", "#9a3412", "#c2410c"]; + +const dateRanges = [ + { label: "7d", days: 7 }, + { label: "30d", days: 30 }, + { label: "90d", days: 90 }, + { label: "All", days: 0 }, +]; + +export default function AnalyticsPage() { + const [data, setData] = useState(null); + const [days, setDays] = useState(30); + const [loading, setLoading] = useState(true); + + const fetchAnalytics = useCallback(async () => { + setLoading(true); + try { + const params = days > 0 ? `?days=${days}` : ""; + const res = await fetch(`/api/admin/analytics${params}`); + if (res.ok) setData(await res.json()); + } catch (err) { + console.error("Failed to fetch analytics:", err); + } finally { + setLoading(false); + } + }, [days]); + + useEffect(() => { + fetchAnalytics(); + }, [fetchAnalytics]); + + const funnelSteps = data?.funnel + ? [ + { label: "Page Views", value: data.funnel.pageViews }, + { label: "Ebook Downloads", value: data.funnel.ebookDownloads }, + { label: "Signups", value: data.funnel.signups }, + { label: "Paid", value: data.funnel.paidCustomers }, + ] + : []; + + const maxFunnel = funnelSteps.length > 0 ? Math.max(funnelSteps[0].value, 1) : 1; + + return ( +
+ {/* Date range selector */} +
+ Period: + {dateRanges.map((range) => ( + + ))} +
+ + {loading ? ( +
+ {Array.from({ length: 4 }).map((_, i) => ( + + +
+ + + ))} +
+ ) : ( +
+ {/* Revenue Over Time */} + + + Revenue Over Time + + +
+ {data?.revenueOverTime?.length ? ( + + + + + + [`$${value.toLocaleString()}`, "Revenue"]} + /> + + + + ) : ( +
+ No data available +
+ )} +
+
+
+ + {/* Sales by Landing Page - Horizontal Bar */} + + + Sales by Landing Page (Top 10) + + +
+ {data?.salesByLP?.length ? ( + + + + + + + + + + ) : ( +
+ No data available +
+ )} +
+
+
+ + {/* Sales by Source - Pie Chart */} + + + Sales by Source + + +
+ {data?.salesBySource?.length ? ( + + + + `${name} (${(percent * 100).toFixed(0)}%)` + } + > + {data.salesBySource.map((_, index) => ( + + ))} + + + + + ) : ( +
+ No data available +
+ )} +
+
+
+ + {/* Conversion Funnel */} + + + Conversion Funnel + + +
+ {funnelSteps.length > 0 ? ( + funnelSteps.map((step, i) => { + const pct = maxFunnel > 0 ? (step.value / maxFunnel) * 100 : 0; + const dropoff = + i > 0 && funnelSteps[i - 1].value > 0 + ? ((step.value / funnelSteps[i - 1].value) * 100).toFixed(1) + : null; + return ( +
+
+ {step.label} +
+ + {step.value.toLocaleString()} + + {dropoff && ( + + ({dropoff}%) + + )} +
+
+
+
+
+
+ ); + }) + ) : ( +
+ No funnel data available +
+ )} +
+ + +
+ )} +
+ ); +} diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx new file mode 100644 index 0000000..e353160 --- /dev/null +++ b/src/app/admin/layout.tsx @@ -0,0 +1,172 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter, usePathname } from "next/navigation"; +import Link from "next/link"; +import { + LayoutDashboard, + ShoppingCart, + Users, + BarChart3, + UserCheck, + Wallet, + LogOut, + Menu, + X, + Plane, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +const navItems = [ + { href: "/admin", label: "Dashboard", icon: LayoutDashboard }, + { href: "/admin/sales", label: "Sales", icon: ShoppingCart }, + { href: "/admin/leads", label: "Leads", icon: Users }, + { href: "/admin/analytics", label: "Analytics", icon: BarChart3 }, + { href: "/admin/affiliates", label: "Affiliates", icon: UserCheck }, + { href: "/admin/payouts", label: "Payouts", icon: Wallet }, +]; + +export default function AdminLayout({ children }: { children: React.ReactNode }) { + const router = useRouter(); + const pathname = usePathname(); + const [authed, setAuthed] = useState(false); + const [checking, setChecking] = useState(true); + const [sidebarOpen, setSidebarOpen] = useState(false); + + // Skip auth check for login page + const isLoginPage = pathname === "/admin/login"; + + useEffect(() => { + if (isLoginPage) { + setChecking(false); + setAuthed(true); + return; + } + + async function checkAuth() { + try { + const res = await fetch("/api/admin/auth/me"); + if (!res.ok) throw new Error("Unauthorized"); + setAuthed(true); + } catch { + router.replace("/admin/login"); + } finally { + setChecking(false); + } + } + checkAuth(); + }, [isLoginPage, router]); + + async function handleLogout() { + document.cookie = "admin_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"; + router.replace("/admin/login"); + } + + if (isLoginPage) { + return <>{children}; + } + + if (checking) { + return ( +
+
+
+

Verifying access...

+
+
+ ); + } + + if (!authed) return null; + + return ( +
+ {/* Mobile overlay */} + {sidebarOpen && ( +
setSidebarOpen(false)} + /> + )} + + {/* Sidebar */} + + + {/* Main content */} +
+ {/* Top bar */} +
+ +

+ {navItems.find((n) => + n.href === "/admin" + ? pathname === "/admin" + : pathname.startsWith(n.href) + )?.label || "Admin"} +

+
+ +
{children}
+
+
+ ); +} diff --git a/src/app/admin/leads/page.tsx b/src/app/admin/leads/page.tsx new file mode 100644 index 0000000..53e8457 --- /dev/null +++ b/src/app/admin/leads/page.tsx @@ -0,0 +1,163 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { Search, ChevronLeft, ChevronRight } from "lucide-react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +interface Lead { + id: string | number; + email: string; + name: string; + source_lp: string; + utm_source: string; + created_at: string; +} + +interface LeadsResponse { + data: Lead[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + +export default function LeadsPage() { + const [data, setData] = useState(null); + const [page, setPage] = useState(1); + const [search, setSearch] = useState(""); + const [searchInput, setSearchInput] = useState(""); + const [loading, setLoading] = useState(true); + const limit = 25; + + const fetchLeads = useCallback(async () => { + setLoading(true); + try { + const params = new URLSearchParams({ page: String(page), limit: String(limit) }); + if (search) params.set("search", search); + const res = await fetch(`/api/admin/leads?${params}`); + if (res.ok) setData(await res.json()); + } catch (err) { + console.error("Failed to fetch leads:", err); + } finally { + setLoading(false); + } + }, [page, search]); + + useEffect(() => { + fetchLeads(); + }, [fetchLeads]); + + function handleSearch(e: React.FormEvent) { + e.preventDefault(); + setPage(1); + setSearch(searchInput); + } + + return ( +
+ + +
+ Ebook Leads +
+
+ + setSearchInput(e.target.value)} + className="pl-9 w-64" + /> +
+ +
+
+
+ +
+ + + + + + + + + + + + + {loading ? ( + Array.from({ length: 5 }).map((_, i) => ( + + {Array.from({ length: 6 }).map((_, j) => ( + + ))} + + )) + ) : data?.data?.length ? ( + data.data.map((lead) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
IDEmailNameSource LPUTM SourceDate
+
+
{lead.id}{lead.email}{lead.name || "-"}{lead.source_lp || "-"}{lead.utm_source || "-"} + {lead.created_at ? new Date(lead.created_at).toLocaleDateString() : "-"} +
+ No leads found +
+
+ + {data && data.totalPages > 1 && ( +
+

+ Showing {(data.page - 1) * data.limit + 1} to{" "} + {Math.min(data.page * data.limit, data.total)} of {data.total} results +

+
+ + + Page {data.page} of {data.totalPages} + + +
+
+ )} +
+
+
+ ); +} diff --git a/src/app/admin/login/page.tsx b/src/app/admin/login/page.tsx new file mode 100644 index 0000000..52bf234 --- /dev/null +++ b/src/app/admin/login/page.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Plane, Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; + +export default function AdminLoginPage() { + const router = useRouter(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(""); + setLoading(true); + + try { + const res = await fetch("/api/admin/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || "Invalid credentials"); + } + + router.push("/admin"); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Login failed"); + } finally { + setLoading(false); + } + } + + return ( +
+ + +
+ +
+ Mexico Paradise Vacations + Admin Portal +
+ +
+ {error && ( +
+ {error} +
+ )} +
+ + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+ +
+
+
+
+ ); +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..fce8bf0 --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,183 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { + DollarSign, + ShoppingCart, + TrendingUp, + Users, + Eye, + Percent, +} from "lucide-react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + ResponsiveContainer, + AreaChart, + Area, + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + CartesianGrid, +} from "recharts"; + +interface Stats { + totalSales: number; + totalRevenue: number; + mrr: number; + totalLeads: number; + totalViews: number; + conversionRate: number; +} + +interface AnalyticsData { + revenueOverTime: { date: string; revenue: number }[]; + salesByLP: { name: string; count: number }[]; +} + +const kpiConfig = [ + { key: "totalSales" as const, label: "Total Sales", icon: ShoppingCart, format: "number" }, + { key: "totalRevenue" as const, label: "Total Revenue", icon: DollarSign, format: "currency" }, + { key: "mrr" as const, label: "MRR", icon: TrendingUp, format: "currency" }, + { key: "totalLeads" as const, label: "Total Leads", icon: Users, format: "number" }, + { key: "totalViews" as const, label: "Page Views", icon: Eye, format: "number" }, + { key: "conversionRate" as const, label: "Conversion Rate", icon: Percent, format: "percent" }, +]; + +function formatValue(value: number, format: string): string { + switch (format) { + case "currency": + return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + case "percent": + return `${value.toFixed(1)}%`; + default: + return value.toLocaleString(); + } +} + +export default function AdminDashboardPage() { + const [stats, setStats] = useState(null); + const [analytics, setAnalytics] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchData() { + try { + const [statsRes, analyticsRes] = await Promise.all([ + fetch("/api/admin/stats"), + fetch("/api/admin/analytics"), + ]); + if (statsRes.ok) setStats(await statsRes.json()); + if (analyticsRes.ok) setAnalytics(await analyticsRes.json()); + } catch (err) { + console.error("Failed to fetch dashboard data:", err); + } finally { + setLoading(false); + } + } + fetchData(); + }, []); + + if (loading) { + return ( +
+
+ {Array.from({ length: 6 }).map((_, i) => ( + + +
+ + + ))} +
+
+ ); + } + + return ( +
+ {/* KPI Cards */} +
+ {kpiConfig.map((kpi) => ( + + +
+
+

{kpi.label}

+

+ {stats ? formatValue(stats[kpi.key], kpi.format) : "--"} +

+
+
+ +
+
+
+
+ ))} +
+ + {/* Charts */} +
+ + + Revenue Over Time + + +
+ {analytics?.revenueOverTime?.length ? ( + + + + + + [`$${value.toLocaleString()}`, "Revenue"]} + /> + + + + ) : ( +
+ No data available +
+ )} +
+
+
+ + + + Sales by Landing Page + + +
+ {analytics?.salesByLP?.length ? ( + + + + + + + + + + ) : ( +
+ No data available +
+ )} +
+
+
+
+
+ ); +} diff --git a/src/app/admin/payouts/page.tsx b/src/app/admin/payouts/page.tsx new file mode 100644 index 0000000..41cd886 --- /dev/null +++ b/src/app/admin/payouts/page.tsx @@ -0,0 +1,301 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Loader2, Plus } from "lucide-react"; + +interface Payout { + id: string | number; + affiliate_name: string; + amount: number; + method: string; + reference: string; + status: string; + created_at: string; +} + +interface Affiliate { + id: string | number; + name: string; + email: string; +} + +const METHODS = [ + { value: "paypal", label: "PayPal" }, + { value: "bank", label: "Bank Transfer" }, + { value: "check", label: "Check" }, + { value: "zelle", label: "Zelle" }, +]; + +function payoutStatusBadge(status: string) { + const s = status?.toLowerCase(); + const styles: Record = { + completed: "bg-green-100 text-green-800 border-green-200", + pending: "bg-yellow-100 text-yellow-800 border-yellow-200", + failed: "bg-red-100 text-red-800 border-red-200", + processing: "bg-blue-100 text-blue-800 border-blue-200", + }; + return ( + + {status} + + ); +} + +export default function PayoutsPage() { + const [payouts, setPayouts] = useState([]); + const [affiliates, setAffiliates] = useState([]); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [showForm, setShowForm] = useState(false); + + // Form state + const [affiliateId, setAffiliateId] = useState(""); + const [amount, setAmount] = useState(""); + const [method, setMethod] = useState(""); + const [reference, setReference] = useState(""); + const [formError, setFormError] = useState(""); + const [formSuccess, setFormSuccess] = useState(""); + + async function fetchData() { + setLoading(true); + try { + const [payoutsRes, affiliatesRes] = await Promise.all([ + fetch("/api/admin/payouts"), + fetch("/api/admin/affiliates"), + ]); + if (payoutsRes.ok) { + const json = await payoutsRes.json(); + setPayouts(json.payouts || json.data || []); + } + if (affiliatesRes.ok) { + const json = await affiliatesRes.json(); + setAffiliates(json.affiliates || []); + } + } catch (err) { + console.error("Failed to fetch payouts:", err); + } finally { + setLoading(false); + } + } + + useEffect(() => { + fetchData(); + }, []); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setFormError(""); + setFormSuccess(""); + + if (!affiliateId || !amount || !method) { + setFormError("Please fill in all required fields."); + return; + } + + setSubmitting(true); + try { + const res = await fetch("/api/admin/payouts", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + affiliate_id: affiliateId, + amount: parseFloat(amount), + method, + reference, + }), + }); + + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || "Failed to create payout"); + } + + setFormSuccess("Payout created successfully."); + setAffiliateId(""); + setAmount(""); + setMethod(""); + setReference(""); + setShowForm(false); + await fetchData(); + } catch (err: unknown) { + setFormError(err instanceof Error ? err.message : "Failed to create payout"); + } finally { + setSubmitting(false); + } + } + + return ( +
+ {/* Create Payout */} + + +
+ Create Payout + +
+
+ {showForm && ( + +
+ {formError && ( +
+ {formError} +
+ )} + {formSuccess && ( +
+ {formSuccess} +
+ )} +
+
+ + +
+
+ + setAmount(e.target.value)} + required + /> +
+
+ + +
+
+ + setReference(e.target.value)} + /> +
+
+ +
+
+ )} +
+ + {/* Payouts Table */} + + + Payout History + + +
+ + + + + + + + + + + + + + {loading ? ( + Array.from({ length: 5 }).map((_, i) => ( + + {Array.from({ length: 7 }).map((_, j) => ( + + ))} + + )) + ) : payouts.length > 0 ? ( + payouts.map((payout) => ( + + + + + + + + + + )) + ) : ( + + + + )} + +
IDAffiliateAmountMethodReferenceStatusDate
+
+
{payout.id}{payout.affiliate_name} + ${Number(payout.amount).toLocaleString("en-US", { minimumFractionDigits: 2 })} + {payout.method}{payout.reference || "-"}{payoutStatusBadge(payout.status)} + {payout.created_at ? new Date(payout.created_at).toLocaleDateString() : "-"} +
+ No payouts found +
+
+
+
+
+ ); +} diff --git a/src/app/admin/sales/page.tsx b/src/app/admin/sales/page.tsx new file mode 100644 index 0000000..1b90881 --- /dev/null +++ b/src/app/admin/sales/page.tsx @@ -0,0 +1,239 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { Search, ChevronLeft, ChevronRight } from "lucide-react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; + +interface Sale { + id: string | number; + full_name: string; + email: string; + amount: number; + payment_status: string; + source_lp: string; + utm_source: string; + affiliate_name: string | null; + created_at: string; + certificate_number: string | null; + certificate_expires: string | null; + payment_plan_months: number | null; + payments_made: number | null; +} + +function validityCell(expires: string | null) { + if (!expires) return -; + const exp = new Date(expires); + const days = Math.ceil((exp.getTime() - Date.now()) / (1000 * 60 * 60 * 24)); + const date = exp.toLocaleDateString(); + let cls = "text-green-700"; + let label = `${days}d left`; + if (days < 0) { cls = "text-red-700 font-medium"; label = `Expired ${Math.abs(days)}d ago`; } + else if (days <= 30) { cls = "text-red-600"; } + else if (days <= 90) { cls = "text-amber-600"; } + return ( +
+ {date} + {label} +
+ ); +} + +function paymentsCell(made: number | null, plan: number | null) { + const m = Number(made ?? 0); + const p = Number(plan ?? 0); + if (!p) return {m}; + const complete = m >= p; + return ( + + {m}/{p} + + ); +} + +interface SalesResponse { + data: Sale[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + +function statusBadge(status: string) { + const s = status?.toLowerCase(); + const styles: Record = { + active: "bg-green-100 text-green-800 border-green-200", + pending: "bg-yellow-100 text-yellow-800 border-yellow-200", + failed: "bg-red-100 text-red-800 border-red-200", + completed: "bg-blue-100 text-blue-800 border-blue-200", + }; + return ( + + {status} + + ); +} + +export default function SalesPage() { + const [data, setData] = useState(null); + const [page, setPage] = useState(1); + const [search, setSearch] = useState(""); + const [searchInput, setSearchInput] = useState(""); + const [loading, setLoading] = useState(true); + const limit = 25; + + const fetchSales = useCallback(async () => { + setLoading(true); + try { + const params = new URLSearchParams({ page: String(page), limit: String(limit) }); + if (search) params.set("search", search); + const res = await fetch(`/api/admin/sales?${params}`); + if (res.ok) setData(await res.json()); + } catch (err) { + console.error("Failed to fetch sales:", err); + } finally { + setLoading(false); + } + }, [page, search]); + + useEffect(() => { + fetchSales(); + }, [fetchSales]); + + function handleSearch(e: React.FormEvent) { + e.preventDefault(); + setPage(1); + setSearch(searchInput); + } + + return ( +
+ + +
+ Sales +
+
+ + setSearchInput(e.target.value)} + className="pl-9 w-64" + /> +
+ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + {loading ? ( + Array.from({ length: 5 }).map((_, i) => ( + + {Array.from({ length: 12 }).map((_, j) => ( + + ))} + + )) + ) : data?.data?.length ? ( + data.data.map((sale) => ( + + + + + + + + + + + + + + + )) + ) : ( + + + + )} + +
IDNameEmailAmountStatusCertificate #Valid UntilPaymentsSource LPUTM SourceAffiliateDate
+
+
{sale.id}{sale.full_name || "-"}{sale.email} + ${Number(sale.amount).toLocaleString("en-US", { minimumFractionDigits: 2 })} + {statusBadge(sale.payment_status)} + {sale.certificate_number ? ( + + {sale.certificate_number} + + ) : ( + - + )} + {validityCell(sale.certificate_expires)}{paymentsCell(sale.payments_made, sale.payment_plan_months)}{sale.source_lp || "-"}{sale.utm_source || "-"}{sale.affiliate_name || "-"} + {sale.created_at ? new Date(sale.created_at).toLocaleDateString() : "-"} +
+ No sales found +
+
+ + {/* Pagination */} + {data && data.totalPages > 1 && ( +
+

+ Showing {(data.page - 1) * data.limit + 1} to{" "} + {Math.min(data.page * data.limit, data.total)} of {data.total} results +

+
+ + + Page {data.page} of {data.totalPages} + + +
+
+ )} +
+
+
+ ); +} diff --git a/src/app/affiliate/layout.tsx b/src/app/affiliate/layout.tsx new file mode 100644 index 0000000..daa1a3f --- /dev/null +++ b/src/app/affiliate/layout.tsx @@ -0,0 +1,217 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter, usePathname } from "next/navigation"; +import Link from "next/link"; +import { LayoutDashboard, Users, Wallet, LogOut, Copy, CheckCircle, Menu, X } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +interface AffiliateData { + name: string; + email: string; + referral_code: string; + commission_rate: number; + status: string; + total_earned: number; + total_paid: number; +} + +const AUTH_FREE_PATHS = ["/affiliate/login", "/affiliate/register"]; + +export default function AffiliateLayout({ children }: { children: React.ReactNode }) { + const router = useRouter(); + const pathname = usePathname(); + const [affiliate, setAffiliate] = useState(null); + const [loading, setLoading] = useState(true); + const [copiedCode, setCopiedCode] = useState(false); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + + const isAuthPage = AUTH_FREE_PATHS.includes(pathname); + + useEffect(() => { + if (isAuthPage) { + setLoading(false); + return; + } + + async function checkAuth() { + try { + const res = await fetch("/api/affiliate/auth/me"); + if (!res.ok) { + router.push("/affiliate/login"); + return; + } + const data = await res.json(); + setAffiliate(data.affiliate); + } catch { + router.push("/affiliate/login"); + } finally { + setLoading(false); + } + } + + checkAuth(); + }, [isAuthPage, router]); + + function handleLogout() { + document.cookie = "affiliate_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"; + router.push("/affiliate/login"); + } + + function handleCopyCode() { + if (affiliate?.referral_code) { + navigator.clipboard.writeText(affiliate.referral_code); + setCopiedCode(true); + setTimeout(() => setCopiedCode(false), 2000); + } + } + + if (isAuthPage) { + return <>{children}; + } + + if (loading) { + return ( +
+
+
+

Loading...

+
+
+ ); + } + + if (!affiliate) { + return null; + } + + const navLinks = [ + { href: "/affiliate", label: "Dashboard", icon: LayoutDashboard }, + { href: "/affiliate/referrals", label: "Referrals", icon: Users }, + { href: "/affiliate/payouts", label: "Payouts", icon: Wallet }, + ]; + + return ( +
+ + +
+ {children} +
+
+ ); +} diff --git a/src/app/affiliate/login/page.tsx b/src/app/affiliate/login/page.tsx new file mode 100644 index 0000000..7600600 --- /dev/null +++ b/src/app/affiliate/login/page.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { Users, Mail, Lock, Loader2 } from "lucide-react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export default function AffiliateLoginPage() { + const router = useRouter(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(""); + setLoading(true); + + try { + const res = await fetch("/api/affiliate/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + + const data = await res.json(); + + if (!res.ok) { + setError(data.error || "Login failed. Please try again."); + return; + } + + router.push("/affiliate"); + } catch { + setError("An unexpected error occurred. Please try again."); + } finally { + setLoading(false); + } + } + + return ( +
+
+
+
+ +
+

Affiliate Portal

+

Mexico Paradise Vacations

+
+ + + + Sign In + Enter your credentials to access your affiliate dashboard + + +
+ {error && ( +
+ {error} +
+ )} + +
+ +
+ + setEmail(e.target.value)} + className="pl-10" + required + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + className="pl-10" + required + /> +
+
+ + +
+ +
+ Don't have an account?{" "} + + Register as an affiliate + +
+
+
+
+
+ ); +} diff --git a/src/app/affiliate/page.tsx b/src/app/affiliate/page.tsx new file mode 100644 index 0000000..8301c57 --- /dev/null +++ b/src/app/affiliate/page.tsx @@ -0,0 +1,259 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { MousePointerClick, ArrowRightLeft, Percent, DollarSign, Clock, CheckCircle, Copy, Loader2, Lightbulb } from "lucide-react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; + +interface AffiliateData { + name: string; + email: string; + referral_code: string; + short_code: string | null; + commission_rate: number; + status: string; + total_earned: number; + total_paid: number; +} + +interface StatsData { + clicks: number; + conversions: number; + conversionRate: number; + totalEarned: number; + pendingAmount: number; + paidAmount: number; +} + +export default function AffiliateDashboardPage() { + const [affiliate, setAffiliate] = useState(null); + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [copied, setCopied] = useState(false); + + useEffect(() => { + async function fetchData() { + try { + const [meRes, statsRes] = await Promise.all([ + fetch("/api/affiliate/auth/me"), + fetch("/api/affiliate/stats"), + ]); + + if (meRes.ok) { + const meData = await meRes.json(); + setAffiliate(meData.affiliate); + } + + if (statsRes.ok) { + const statsData = await statsRes.json(); + setStats(statsData); + } + } catch { + // Errors handled by layout auth check + } finally { + setLoading(false); + } + } + + fetchData(); + }, []); + + const [copiedKey, setCopiedKey] = useState(null); + function copy(text: string, key: string) { + navigator.clipboard.writeText(text); + setCopiedKey(key); + setTimeout(() => setCopiedKey(null), 2000); + } + function handleCopyLink() { + if (affiliate?.referral_code) { + copy(`https://hi2b.com/lp/golden-hour?ref=${affiliate.referral_code}`, 'long'); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + } + + if (loading) { + return ( +
+ +
+ ); + } + + const referralLink = affiliate + ? `https://hi2b.com/lp/golden-hour?ref=${affiliate.referral_code}` + : ""; + const shortLink = affiliate?.short_code + ? `https://hi2b.com/pay/${affiliate.short_code}` + : ""; + + const kpis = [ + { + label: "Clicks", + value: stats?.clicks ?? 0, + format: "number", + icon: MousePointerClick, + color: "text-blue-600", + bg: "bg-blue-50", + }, + { + label: "Conversions", + value: stats?.conversions ?? 0, + format: "number", + icon: ArrowRightLeft, + color: "text-emerald-600", + bg: "bg-emerald-50", + }, + { + label: "Conversion Rate", + value: stats?.conversionRate ?? 0, + format: "percent", + icon: Percent, + color: "text-purple-600", + bg: "bg-purple-50", + }, + { + label: "Total Earned", + value: stats?.totalEarned ?? 0, + format: "currency", + icon: DollarSign, + color: "text-teal-600", + bg: "bg-teal-50", + }, + { + label: "Pending", + value: stats?.pendingAmount ?? 0, + format: "currency", + icon: Clock, + color: "text-amber-600", + bg: "bg-amber-50", + }, + { + label: "Paid", + value: stats?.paidAmount ?? 0, + format: "currency", + icon: CheckCircle, + color: "text-green-600", + bg: "bg-green-50", + }, + ]; + + function formatValue(value: number | string | null | undefined, format: string): string { + const n = typeof value === "number" ? value : Number(value) || 0; + switch (format) { + case "currency": + return `$${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + case "percent": + return `${n.toFixed(1)}%`; + default: + return n.toLocaleString(); + } + } + + return ( +
+ {/* Welcome Header */} +
+

+ Welcome back, {affiliate?.name ?? "Affiliate"} +

+

Here's an overview of your affiliate performance.

+
+ + {/* Short link — easy-to-text, easy-to-remember */} + {shortLink && ( + + +
+
+

+ 📲 Your Short Link — perfect for texts, IG bio, business cards +

+ + {shortLink} + +
+ +
+
+
+ )} + + {/* Long referral link box */} + + +
+
+

Full Referral Link — landing page version with tracking

+ + {referralLink} + +
+ +
+
+
+ + {/* KPI Cards */} +
+ {kpis.map((kpi) => ( + + + {kpi.label} +
+ +
+
+ +

+ {formatValue(kpi.value, kpi.format)} +

+
+
+ ))} +
+ + {/* Quick Tip */} + + +
+
+ +
+
+

Quick Tip

+

+ Share your referral link on social media, email, or your website. You earn{" "} + {affiliate?.commission_rate ?? 0}% commission on every sale! +

+
+
+
+
+
+ ); +} diff --git a/src/app/affiliate/payouts/page.tsx b/src/app/affiliate/payouts/page.tsx new file mode 100644 index 0000000..e121ca2 --- /dev/null +++ b/src/app/affiliate/payouts/page.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Loader2, Wallet, Inbox } from "lucide-react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +interface Payout { + id: string; + amount: number; + method: string; + reference: string; + status: string; + created_at: string; +} + +const STATUS_STYLES: Record = { + pending: "bg-yellow-100 text-yellow-800 border-yellow-200", + processing: "bg-blue-100 text-blue-800 border-blue-200", + completed: "bg-green-100 text-green-800 border-green-200", + failed: "bg-red-100 text-red-800 border-red-200", +}; + +export default function AffiliatePayoutsPage() { + const [payouts, setPayouts] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchPayouts() { + try { + const res = await fetch("/api/affiliate/payouts"); + if (res.ok) { + const data = await res.json(); + setPayouts(data.payouts || []); + } + } catch { + // Error handled silently + } finally { + setLoading(false); + } + } + + fetchPayouts(); + }, []); + + function formatDate(dateStr: string): string { + return new Date(dateStr).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + } + + function formatCurrency(amount: number): string { + return `$${amount.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + } + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+
+

Payouts

+

View your payout history and payment details.

+
+ + + +
+ + Payout History +
+ + {payouts.length} total payout{payouts.length !== 1 ? "s" : ""} + +
+ + {payouts.length === 0 ? ( +
+
+ +
+

No payouts yet

+

+ Keep sharing your referral link! Payouts will appear here once processed. +

+
+ ) : ( +
+ + + + Date + Amount + Method + Reference + Status + + + + {payouts.map((payout) => ( + + + {formatDate(payout.created_at)} + + + {formatCurrency(payout.amount)} + + + {payout.method} + + + {payout.reference || "—"} + + + + {payout.status.charAt(0).toUpperCase() + payout.status.slice(1)} + + + + ))} + +
+
+ )} +
+
+
+ ); +} diff --git a/src/app/affiliate/referrals/page.tsx b/src/app/affiliate/referrals/page.tsx new file mode 100644 index 0000000..3a67d11 --- /dev/null +++ b/src/app/affiliate/referrals/page.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Loader2, Users, Inbox } from "lucide-react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +interface Referral { + id: string; + commission_amount: number; + status: string; + created_at: string; + paid_at: string | null; + customer_email: string; + sale_amount: number; +} + +const STATUS_STYLES: Record = { + pending: "bg-yellow-100 text-yellow-800 border-yellow-200", + approved: "bg-blue-100 text-blue-800 border-blue-200", + paid: "bg-green-100 text-green-800 border-green-200", + rejected: "bg-red-100 text-red-800 border-red-200", +}; + +export default function AffiliateReferralsPage() { + const [referrals, setReferrals] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchReferrals() { + try { + const res = await fetch("/api/affiliate/referrals"); + if (res.ok) { + const data = await res.json(); + setReferrals(data.referrals || []); + } + } catch { + // Error handled silently + } finally { + setLoading(false); + } + } + + fetchReferrals(); + }, []); + + function formatDate(dateStr: string): string { + return new Date(dateStr).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + } + + function formatCurrency(amount: number): string { + return `$${amount.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + } + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+
+

Referrals

+

Track all your referral sales and commissions.

+
+ + + +
+ + Referral History +
+ + {referrals.length} total referral{referrals.length !== 1 ? "s" : ""} + +
+ + {referrals.length === 0 ? ( +
+
+ +
+

No referrals yet

+

+ Share your referral link to start earning commissions. +

+
+ ) : ( +
+ + + + Date + Customer + Sale Amount + Commission + Status + + + + {referrals.map((referral) => ( + + + {formatDate(referral.created_at)} + + + {referral.customer_email} + + + {formatCurrency(referral.sale_amount)} + + + {formatCurrency(referral.commission_amount)} + + + + {referral.status.charAt(0).toUpperCase() + referral.status.slice(1)} + + + + ))} + +
+
+ )} +
+
+
+ ); +} diff --git a/src/app/affiliate/register/page.tsx b/src/app/affiliate/register/page.tsx new file mode 100644 index 0000000..b982b13 --- /dev/null +++ b/src/app/affiliate/register/page.tsx @@ -0,0 +1,220 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { Users, Mail, Lock, User, Loader2, CheckCircle, Copy } from "lucide-react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export default function AffiliateRegisterPage() { + const router = useRouter(); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + const [referralCode, setReferralCode] = useState(""); + const [copied, setCopied] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(""); + + if (password !== confirmPassword) { + setError("Passwords do not match."); + return; + } + + if (password.length < 6) { + setError("Password must be at least 6 characters."); + return; + } + + setLoading(true); + + try { + const res = await fetch("/api/affiliate/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, email, password }), + }); + + const data = await res.json(); + + if (!res.ok) { + setError(data.error || "Registration failed. Please try again."); + return; + } + + setReferralCode(data.referral_code || data.affiliate?.referral_code || ""); + setSuccess(true); + } catch { + setError("An unexpected error occurred. Please try again."); + } finally { + setLoading(false); + } + } + + function handleCopyCode() { + navigator.clipboard.writeText(referralCode); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + + if (success) { + return ( +
+
+ + +
+ +
+

Registration Successful!

+

+ Welcome to the Mexico Paradise Vacations affiliate program. Your referral code is: +

+ {referralCode && ( +
+ + {referralCode} + + +
+ )} + +
+
+
+
+ ); + } + + return ( +
+
+
+
+ +
+

Join Our Affiliate Program

+

Mexico Paradise Vacations

+
+ + + + Create Account + Register to start earning commissions on referrals + + +
+ {error && ( +
+ {error} +
+ )} + +
+ +
+ + setName(e.target.value)} + className="pl-10" + required + /> +
+
+ +
+ +
+ + setEmail(e.target.value)} + className="pl-10" + required + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + className="pl-10" + required + /> +
+
+ +
+ +
+ + setConfirmPassword(e.target.value)} + className="pl-10" + required + /> +
+
+ + +
+ +
+ Already have an account?{" "} + + Sign in + +
+
+
+
+
+ ); +} diff --git a/src/app/api/admin/affiliates/route.ts b/src/app/api/admin/affiliates/route.ts new file mode 100644 index 0000000..933c510 --- /dev/null +++ b/src/app/api/admin/affiliates/route.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getAdminSession } from '@/lib/admin-auth' +import { getAffiliates, updateAffiliateStatus } from '@/lib/db-admin' + +export async function GET() { + const session = await getAdminSession() + if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + const affiliates = await getAffiliates() + return NextResponse.json({ affiliates }) +} + +export async function PUT(request: NextRequest) { + const session = await getAdminSession() + if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + + const { id, status } = await request.json() + if (!id || !status) return NextResponse.json({ error: 'Missing fields' }, { status: 400 }) + + await updateAffiliateStatus(id, status) + return NextResponse.json({ success: true }) +} diff --git a/src/app/api/admin/analytics/route.ts b/src/app/api/admin/analytics/route.ts new file mode 100644 index 0000000..15cdf57 --- /dev/null +++ b/src/app/api/admin/analytics/route.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getAdminSession } from '@/lib/admin-auth' +import { getSalesByLP, getSalesBySource, getSalesByAffiliate, getRevenueOverTime, getFunnelData } from '@/lib/db-admin' + +export async function GET(request: NextRequest) { + const session = await getAdminSession() + if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + + const url = new URL(request.url) + const days = parseInt(url.searchParams.get('days') || '30') + + const [salesByLP, salesBySource, salesByAffiliate, revenueOverTime, funnel] = await Promise.all([ + getSalesByLP(), + getSalesBySource(), + getSalesByAffiliate(), + getRevenueOverTime(days), + getFunnelData(days), + ]) + + return NextResponse.json({ salesByLP, salesBySource, salesByAffiliate, revenueOverTime, funnel }) +} diff --git a/src/app/api/admin/auth/login/route.ts b/src/app/api/admin/auth/login/route.ts new file mode 100644 index 0000000..9864c3c --- /dev/null +++ b/src/app/api/admin/auth/login/route.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/db-mysql' +import { verifyPassword, createAdminToken } from '@/lib/admin-auth' + +export async function POST(request: NextRequest) { + try { + const { email, password } = await request.json() + if (!email || !password) return NextResponse.json({ error: 'Email and password required' }, { status: 400 }) + + const [rows] = await pool.execute('SELECT * FROM admin_users WHERE email = ? AND status = ?', [email, 'active']) as any[] + if (rows.length === 0) return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 }) + + const admin = rows[0] + const valid = await verifyPassword(password, admin.password_hash) + if (!valid) return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 }) + + const token = createAdminToken({ id: admin.id, email: admin.email, role: admin.role }) + const response = NextResponse.json({ success: true, user: { id: admin.id, email: admin.email, fullName: admin.full_name, role: admin.role } }) + response.cookies.set('admin_token', token, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 24 * 60 * 60, path: '/' }) + return response + } catch (error) { + console.error('Admin login error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/src/app/api/admin/auth/me/route.ts b/src/app/api/admin/auth/me/route.ts new file mode 100644 index 0000000..e0d037c --- /dev/null +++ b/src/app/api/admin/auth/me/route.ts @@ -0,0 +1,8 @@ +import { NextResponse } from 'next/server' +import { getAdminSession } from '@/lib/admin-auth' + +export async function GET() { + const session = await getAdminSession() + if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }) + return NextResponse.json({ user: session }) +} diff --git a/src/app/api/admin/leads/route.ts b/src/app/api/admin/leads/route.ts new file mode 100644 index 0000000..44d2d88 --- /dev/null +++ b/src/app/api/admin/leads/route.ts @@ -0,0 +1,17 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getAdminSession } from '@/lib/admin-auth' +import { getLeads } from '@/lib/db-admin' + +export async function GET(request: NextRequest) { + const session = await getAdminSession() + if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + + const url = new URL(request.url) + const result = await getLeads({ + page: parseInt(url.searchParams.get('page') || '1'), + limit: parseInt(url.searchParams.get('limit') || '25'), + search: url.searchParams.get('search') || undefined, + }) + + return NextResponse.json(result) +} diff --git a/src/app/api/admin/payouts/route.ts b/src/app/api/admin/payouts/route.ts new file mode 100644 index 0000000..a786d46 --- /dev/null +++ b/src/app/api/admin/payouts/route.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getAdminSession } from '@/lib/admin-auth' +import { getPayouts, createPayout } from '@/lib/db-admin' + +export async function GET() { + const session = await getAdminSession() + if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + const payouts = await getPayouts() + return NextResponse.json({ payouts }) +} + +export async function POST(request: NextRequest) { + const session = await getAdminSession() + if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + + const { affiliate_id, amount, method, reference } = await request.json() + if (!affiliate_id || !amount) return NextResponse.json({ error: 'Missing fields' }, { status: 400 }) + + const id = await createPayout(affiliate_id, amount, method || 'paypal', reference || '') + return NextResponse.json({ success: true, id }) +} diff --git a/src/app/api/admin/sales/route.ts b/src/app/api/admin/sales/route.ts new file mode 100644 index 0000000..45905fa --- /dev/null +++ b/src/app/api/admin/sales/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getAdminSession } from '@/lib/admin-auth' +import { getSales } from '@/lib/db-admin' + +export async function GET(request: NextRequest) { + const session = await getAdminSession() + if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + + const url = new URL(request.url) + const result = await getSales({ + page: parseInt(url.searchParams.get('page') || '1'), + limit: parseInt(url.searchParams.get('limit') || '25'), + status: url.searchParams.get('status') || undefined, + source_lp: url.searchParams.get('source_lp') || undefined, + utm_source: url.searchParams.get('utm_source') || undefined, + search: url.searchParams.get('search') || undefined, + }) + + return NextResponse.json(result) +} diff --git a/src/app/api/admin/stats/route.ts b/src/app/api/admin/stats/route.ts new file mode 100644 index 0000000..27092e1 --- /dev/null +++ b/src/app/api/admin/stats/route.ts @@ -0,0 +1,11 @@ +import { NextResponse } from 'next/server' +import { getAdminSession } from '@/lib/admin-auth' +import { getKPIStats } from '@/lib/db-admin' + +export async function GET() { + const session = await getAdminSession() + if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + + const stats = await getKPIStats() + return NextResponse.json(stats) +} diff --git a/src/app/api/affiliate/auth/login/route.ts b/src/app/api/affiliate/auth/login/route.ts new file mode 100644 index 0000000..89c8e10 --- /dev/null +++ b/src/app/api/affiliate/auth/login/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/db-mysql' +import { verifyPassword, createAffiliateToken } from '@/lib/admin-auth' + +export async function POST(request: NextRequest) { + try { + const { email, password } = await request.json() + if (!email || !password) return NextResponse.json({ error: 'Email and password required' }, { status: 400 }) + + const [rows] = await pool.execute('SELECT * FROM affiliates WHERE email = ?', [email]) as any[] + if (rows.length === 0) return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 }) + + const aff = rows[0] + if (aff.status === 'suspended') return NextResponse.json({ error: 'Account suspended' }, { status: 403 }) + + const valid = await verifyPassword(password, aff.password_hash) + if (!valid) return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 }) + + const token = createAffiliateToken({ id: aff.id, email: aff.email, referral_code: aff.referral_code }) + const response = NextResponse.json({ success: true, referralCode: aff.referral_code }) + response.cookies.set('affiliate_token', token, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 7 * 24 * 60 * 60, path: '/' }) + return response + } catch (error) { + console.error('Affiliate login error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/src/app/api/affiliate/auth/me/route.ts b/src/app/api/affiliate/auth/me/route.ts new file mode 100644 index 0000000..6b834ff --- /dev/null +++ b/src/app/api/affiliate/auth/me/route.ts @@ -0,0 +1,20 @@ +import { NextResponse } from 'next/server' +import pool, { ensureAffiliateShortCode } from '@/lib/db-mysql' +import { getAffiliateSession } from '@/lib/admin-auth' + +export async function GET() { + const session = await getAffiliateSession() + if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }) + + // Ensure this affiliate has a 2-char short_code for the /pay/XX link + let short_code: string | null = null + try { short_code = await ensureAffiliateShortCode(session.id) } catch (err) { console.error('short_code provisioning:', err) } + + const [rows] = await pool.execute( + 'SELECT id, name, email, referral_code, short_code, commission_rate, status, total_earned, total_paid, created_at FROM affiliates WHERE id = ?', + [session.id] + ) as any[] + + if (rows.length === 0) return NextResponse.json({ error: 'Not found' }, { status: 404 }) + return NextResponse.json({ affiliate: { ...rows[0], short_code: rows[0].short_code || short_code } }) +} diff --git a/src/app/api/affiliate/auth/register/route.ts b/src/app/api/affiliate/auth/register/route.ts new file mode 100644 index 0000000..8f324fe --- /dev/null +++ b/src/app/api/affiliate/auth/register/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/db-mysql' +import { hashPassword, createAffiliateToken, generateReferralCode } from '@/lib/admin-auth' +import { sendAffiliateWelcomeEmail } from '@/lib/email' + +export async function POST(request: NextRequest) { + try { + const { name, email, password } = await request.json() + if (!name || !email || !password) return NextResponse.json({ error: 'All fields required' }, { status: 400 }) + if (password.length < 6) return NextResponse.json({ error: 'Password must be at least 6 characters' }, { status: 400 }) + + const [existing] = await pool.execute('SELECT id FROM affiliates WHERE email = ?', [email]) as any[] + if (existing.length > 0) return NextResponse.json({ error: 'Email already registered' }, { status: 409 }) + + const hash = await hashPassword(password) + let referralCode = generateReferralCode(name) + + // Ensure uniqueness + for (let i = 0; i < 10; i++) { + const [dup] = await pool.execute('SELECT id FROM affiliates WHERE referral_code = ?', [referralCode]) as any[] + if (dup.length === 0) break + referralCode = generateReferralCode(name) + } + + const [result] = await pool.execute( + `INSERT INTO affiliates (name, email, password_hash, referral_code, status) VALUES (?, ?, ?, ?, 'active')`, + [name, email, hash, referralCode] + ) as any[] + + const id = (result as any).insertId + const token = createAffiliateToken({ id, email, referral_code: referralCode }) + + try { await sendAffiliateWelcomeEmail(email, name, referralCode) } catch (e) { console.error('Affiliate email failed:', e) } + + const response = NextResponse.json({ success: true, referralCode }) + response.cookies.set('affiliate_token', token, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 7 * 24 * 60 * 60, path: '/' }) + return response + } catch (error) { + console.error('Affiliate register error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/src/app/api/affiliate/payouts/route.ts b/src/app/api/affiliate/payouts/route.ts new file mode 100644 index 0000000..9112faa --- /dev/null +++ b/src/app/api/affiliate/payouts/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from 'next/server' +import pool from '@/lib/db-mysql' +import { getAffiliateSession } from '@/lib/admin-auth' + +export async function GET() { + const session = await getAffiliateSession() + if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + + const [rows] = await pool.execute( + 'SELECT * FROM affiliate_payouts WHERE affiliate_id = ? ORDER BY created_at DESC', + [session.id] + ) as any[] + + return NextResponse.json({ payouts: rows }) +} diff --git a/src/app/api/affiliate/referrals/route.ts b/src/app/api/affiliate/referrals/route.ts new file mode 100644 index 0000000..98d16d4 --- /dev/null +++ b/src/app/api/affiliate/referrals/route.ts @@ -0,0 +1,20 @@ +import { NextResponse } from 'next/server' +import pool from '@/lib/db-mysql' +import { getAffiliateSession } from '@/lib/admin-auth' + +export async function GET() { + const session = await getAffiliateSession() + if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + + const [rows] = await pool.execute( + `SELECT ar.id, ar.commission_amount, ar.status, ar.created_at, ar.paid_at, + CONCAT(LEFT(s.email, 3), '***@***') as customer_email, s.amount as sale_amount + FROM affiliate_referrals ar + JOIN signups s ON ar.signup_id = s.id + WHERE ar.affiliate_id = ? + ORDER BY ar.created_at DESC`, + [session.id] + ) as any[] + + return NextResponse.json({ referrals: rows }) +} diff --git a/src/app/api/affiliate/stats/route.ts b/src/app/api/affiliate/stats/route.ts new file mode 100644 index 0000000..8c3563f --- /dev/null +++ b/src/app/api/affiliate/stats/route.ts @@ -0,0 +1,36 @@ +import { NextResponse } from 'next/server' +import pool from '@/lib/db-mysql' +import { getAffiliateSession } from '@/lib/admin-auth' + +export async function GET() { + const session = await getAffiliateSession() + if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + + const [clicks] = await pool.execute( + 'SELECT COUNT(*) as c FROM page_views WHERE referral_code = ?', [session.referral_code] + ) as any[] + + const [conversions] = await pool.execute( + 'SELECT COUNT(*) as c, COALESCE(SUM(commission_amount),0) as total FROM affiliate_referrals WHERE affiliate_id = ?', [session.id] + ) as any[] + + const [pending] = await pool.execute( + `SELECT COALESCE(SUM(commission_amount),0) as amount FROM affiliate_referrals WHERE affiliate_id = ? AND status = 'pending'`, [session.id] + ) as any[] + + const [paid] = await pool.execute( + `SELECT COALESCE(SUM(amount),0) as amount FROM affiliate_payouts WHERE affiliate_id = ? AND status = 'completed'`, [session.id] + ) as any[] + + const totalClicks = Number(clicks[0]?.c) || 0 + const totalConversions = Number(conversions[0]?.c) || 0 + + return NextResponse.json({ + clicks: totalClicks, + conversions: totalConversions, + conversionRate: totalClicks > 0 ? (totalConversions / totalClicks) * 100 : 0, + totalEarned: Number(conversions[0]?.total) || 0, + pendingAmount: Number(pending[0]?.amount) || 0, + paidAmount: Number(paid[0]?.amount) || 0, + }) +} diff --git a/src/app/api/auth/forgot-password/route.ts b/src/app/api/auth/forgot-password/route.ts new file mode 100644 index 0000000..c678b07 --- /dev/null +++ b/src/app/api/auth/forgot-password/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/db-mysql' +import { generateResetToken } from '@/lib/auth-utils' +import { sendPasswordResetEmail } from '@/lib/email' + +export async function POST(request: NextRequest) { + try { + const { email } = await request.json() + if (!email) { + return NextResponse.json({ error: 'Email required' }, { status: 400 }) + } + + const [rows] = await pool.execute('SELECT id FROM signups WHERE email = ?', [email]) as any[] + + // Always return success to prevent email enumeration + if (rows.length === 0) { + return NextResponse.json({ success: true, message: 'If an account exists, a reset email has been sent.' }) + } + + const token = generateResetToken() + const expires = new Date(Date.now() + 60 * 60 * 1000) // 1 hour + + await pool.execute( + 'UPDATE signups SET reset_token = ?, reset_token_expires = ? WHERE email = ?', + [token, expires.toISOString().slice(0, 19).replace('T', ' '), email] + ) + + try { + await sendPasswordResetEmail(email, token) + } catch (emailErr) { + console.error('Email send failed:', emailErr) + // Still return success — don't leak email delivery status + } + + return NextResponse.json({ success: true, message: 'If an account exists, a reset email has been sent.' }) + } catch (error) { + console.error('Forgot password error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..607224c --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/db-mysql' +import { verifyPassword, createToken } from '@/lib/auth-utils' + +export async function POST(request: NextRequest) { + try { + const { email, password } = await request.json() + if (!email || !password) { + return NextResponse.json({ error: 'Email and password required' }, { status: 400 }) + } + + const [rows] = await pool.execute('SELECT * FROM signups WHERE email = ?', [email]) as any[] + if (rows.length === 0) { + return NextResponse.json({ error: 'Invalid email or password' }, { status: 401 }) + } + + const user = rows[0] + if (!user.password_hash) { + return NextResponse.json({ error: 'Please set your password first. Check your email or use "Forgot Password".' }, { status: 401 }) + } + + const valid = await verifyPassword(password, user.password_hash) + if (!valid) { + return NextResponse.json({ error: 'Invalid email or password' }, { status: 401 }) + } + + const token = createToken({ id: user.id, email: user.email }) + + const response = NextResponse.json({ + success: true, + user: { id: user.id, email: user.email, full_name: user.full_name }, + }) + + response.cookies.set('auth_token', token, { + httpOnly: true, + secure: true, + sameSite: 'lax', + maxAge: 7 * 24 * 60 * 60, + path: '/', + }) + + return response + } catch (error) { + console.error('Login error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts new file mode 100644 index 0000000..3b32a84 --- /dev/null +++ b/src/app/api/auth/me/route.ts @@ -0,0 +1,50 @@ +import { NextResponse } from 'next/server' +import pool from '@/lib/db-mysql' +import { getSessionUser } from '@/lib/auth-utils' + +export async function GET() { + try { + const session = await getSessionUser() + if (!session) { + return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }) + } + + const [rows] = await pool.execute( + 'SELECT id, email, full_name, phone, destination, amount, monthly_payment, payment_plan_months, payment_status, certificate_number, certificate_expires, created_at FROM signups WHERE id = ?', + [session.id] + ) as any[] + + if (rows.length === 0) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }) + } + + const user = rows[0] + + // Get payments + const [payments] = await pool.execute( + 'SELECT id, amount, currency, payment_type, status, transaction_id, created_at FROM payments WHERE signup_id = ? ORDER BY created_at DESC', + [user.id] + ) as any[] + + return NextResponse.json({ + user: { + id: user.id, + email: user.email, + fullName: user.full_name, + phone: user.phone, + destination: user.destination, + totalAmount: user.amount, + monthlyPayment: user.monthly_payment, + paymentPlanMonths: user.payment_plan_months, + paymentStatus: user.payment_status, + certificateNumber: user.certificate_number, + certificateExpires: user.certificate_expires, + createdAt: user.created_at, + }, + payments, + }) + } catch (error) { + console.error('Me error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts new file mode 100644 index 0000000..e3e58fb --- /dev/null +++ b/src/app/api/auth/register/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/db-mysql' +import { hashPassword, createToken, generateCertificateNumber } from '@/lib/auth-utils' + +export async function POST(request: NextRequest) { + try { + const { email, password } = await request.json() + if (!email || !password) { + return NextResponse.json({ error: 'Email and password required' }, { status: 400 }) + } + if (password.length < 6) { + return NextResponse.json({ error: 'Password must be at least 6 characters' }, { status: 400 }) + } + + const [rows] = await pool.execute('SELECT * FROM signups WHERE email = ?', [email]) as any[] + if (rows.length === 0) { + return NextResponse.json({ error: 'No account found with this email. Please purchase a certificate first.' }, { status: 404 }) + } + + const user = rows[0] + if (user.password_hash) { + return NextResponse.json({ error: 'Password already set. Please log in.' }, { status: 409 }) + } + + const hash = await hashPassword(password) + const certNumber = user.certificate_number || generateCertificateNumber() + const certExpires = user.certificate_expires || new Date(Date.now() + 18 * 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] + + await pool.execute( + 'UPDATE signups SET password_hash = ?, certificate_number = ?, certificate_expires = ? WHERE id = ?', + [hash, certNumber, certExpires, user.id] + ) + + const token = createToken({ id: user.id, email: user.email }) + + const response = NextResponse.json({ + success: true, + user: { id: user.id, email: user.email, full_name: user.full_name }, + }) + + response.cookies.set('auth_token', token, { + httpOnly: true, secure: true, sameSite: 'lax', + maxAge: 7 * 24 * 60 * 60, path: '/', + }) + + return response + } catch (error) { + console.error('Register error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/src/app/api/auth/reset-password/route.ts b/src/app/api/auth/reset-password/route.ts new file mode 100644 index 0000000..0605229 --- /dev/null +++ b/src/app/api/auth/reset-password/route.ts @@ -0,0 +1,35 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/db-mysql' +import { hashPassword } from '@/lib/auth-utils' + +export async function POST(request: NextRequest) { + try { + const { token, password } = await request.json() + if (!token || !password) { + return NextResponse.json({ error: 'Token and password required' }, { status: 400 }) + } + if (password.length < 6) { + return NextResponse.json({ error: 'Password must be at least 6 characters' }, { status: 400 }) + } + + const [rows] = await pool.execute( + 'SELECT * FROM signups WHERE reset_token = ? AND reset_token_expires > NOW()', + [token] + ) as any[] + + if (rows.length === 0) { + return NextResponse.json({ error: 'Invalid or expired reset token' }, { status: 400 }) + } + + const hash = await hashPassword(password) + await pool.execute( + 'UPDATE signups SET password_hash = ?, reset_token = NULL, reset_token_expires = NULL WHERE id = ?', + [hash, rows[0].id] + ) + + return NextResponse.json({ success: true, message: 'Password updated. You can now log in.' }) + } catch (error) { + console.error('Reset password error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/src/app/api/claim/route.ts b/src/app/api/claim/route.ts new file mode 100644 index 0000000..3928232 --- /dev/null +++ b/src/app/api/claim/route.ts @@ -0,0 +1,100 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool, { upsertEarlyLead, createEbookLead } from '@/lib/db-mysql' +import { sendVerifyEmail, sendAdminNotify } from '@/lib/email' +import { makeVerifyToken } from '@/lib/verify-token' + +/** + * Unified "Claim My Certificate" endpoint. Fires on every primary CTA click. + * + * 1. Persists email + phone + IP to early_leads (confirmed=1 — they clicked) + * 2. Creates an ebook_lead so /admin/leads shows them + * 3. Sends the PDF guide email (the same one the ebook form has always sent) + * 4. Attributes to affiliate if a referral_code is present + * + * Returns immediately even if the email send is in-flight, so the UI can + * keep moving (open the payment modal, etc.). + */ + +const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ +const APP_URL = process.env.NEXT_PUBLIC_APP_URL || 'https://hi2b.com' + +function clientIp(req: NextRequest): string | null { + const fwd = req.headers.get('x-forwarded-for') + if (fwd) return fwd.split(',')[0].trim() + return req.headers.get('x-real-ip') || req.headers.get('cf-connecting-ip') || null +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json().catch(() => ({})) + const { + email: rawEmail, phone, name, source_lp, + referral_code, utm_source, utm_medium, utm_campaign, + } = body + + if (!rawEmail || typeof rawEmail !== 'string' || !EMAIL_RE.test(rawEmail)) { + return NextResponse.json({ success: false, error: 'Valid email required' }, { status: 400 }) + } + const email = rawEmail.trim().toLowerCase() + const ip = clientIp(request) + + // 1 + 2 — persist lead (run in parallel) + await Promise.all([ + upsertEarlyLead({ + email, + phone: phone || null, + name: name || null, + ip_address: ip, + source_lp: source_lp || null, + referral_code: referral_code || null, + utm_source: utm_source || null, + utm_medium: utm_medium || null, + utm_campaign: utm_campaign || null, + }), + createEbookLead({ + email, name: name || undefined, + source_lp: source_lp || 'claim-cta', + utm_source, utm_medium, utm_campaign, + }), + ]) + + // NOTE: lead is added but left non-active (early_leads.confirmed = 0). + // It only flips to active when the user clicks the verify link (/api/verify). + + // Notify the team of the new email (fire-and-forget) + sendAdminNotify('New email captured — hi2b', { + Email: email, Phone: phone, Name: name, + 'Source LP': source_lp, 'UTM Source': utm_source, 'UTM Campaign': utm_campaign, + Referral: referral_code, IP: ip, Status: 'unverified (pending double opt-in)', + }).catch(err => console.error('adminNotify claim:', err)) + + // Attribute to affiliate if referral_code provided + if (referral_code) { + pool.execute( + 'UPDATE ebook_leads SET affiliate_id = (SELECT id FROM affiliates WHERE referral_code = ? AND status = ?) WHERE email = ?', + [referral_code, 'active', email] + ).catch(err => console.error('Affiliate attribution:', err)) + } + + // 3 — send the double opt-in verification email (link only; the PDF is + // delivered after they click and verify). + const verifyUrl = `${APP_URL}/api/verify?e=${encodeURIComponent(email)}&t=${makeVerifyToken(email)}` + let emailSent = true + try { + const result = await sendVerifyEmail(email, name, verifyUrl) + if (!result) emailSent = false + } catch (err) { + console.error('sendVerifyEmail failed:', err) + emailSent = false + } + + return NextResponse.json({ + success: true, + emailSent, + needsVerification: true, + }) + } catch (error) { + console.error('claim error:', error) + return NextResponse.json({ success: false, error: 'Internal error' }, { status: 500 }) + } +} diff --git a/src/app/api/ebook/route.ts b/src/app/api/ebook/route.ts new file mode 100644 index 0000000..3ba04c2 --- /dev/null +++ b/src/app/api/ebook/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/db-mysql' +import { createEbookLead, confirmEarlyLead } from '@/lib/db-mysql' +import { sendEbookEmail } from '@/lib/email' + +export async function POST(request: NextRequest) { + try { + const { email, name, source_lp, referral_code, utm_source, utm_medium, utm_campaign } = await request.json() + if (!email) return NextResponse.json({ error: 'Email is required' }, { status: 400 }) + + // Look up affiliate by referral code + let affiliateId: number | null = null + if (referral_code) { + const [aff] = await pool.execute( + 'SELECT id FROM affiliates WHERE referral_code = ? AND status = ?', + [referral_code, 'active'] + ) as any[] + if (aff.length > 0) affiliateId = aff[0].id + } + + // Store lead with affiliate tracking + await createEbookLead({ email, name, source_lp, utm_source, utm_medium, utm_campaign }) + + // Update affiliate_id on the lead (createEbookLead doesn't have it yet) + if (affiliateId) { + await pool.execute( + 'UPDATE ebook_leads SET affiliate_id = ? WHERE email = ?', + [affiliateId, email] + ) + } + + try { await confirmEarlyLead(email.trim().toLowerCase()) } catch {} + + // Send ebook email + try { await sendEbookEmail(email, name) } catch (e) { console.error('Ebook email failed:', e) } + + return NextResponse.json({ success: true, downloadUrl: '/ebooks/budget-luxury-travel.pdf' }) + } catch (error) { + console.error('Ebook API error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/src/app/api/payment/create/route.ts b/src/app/api/payment/create/route.ts index d20d46e..c80a67a 100644 --- a/src/app/api/payment/create/route.ts +++ b/src/app/api/payment/create/route.ts @@ -1,86 +1,91 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabase } from '@/lib/supabase' -import { maverickAPI } from '@/lib/maverick' +import { createPayment, updateSignup, createAffiliateReferral } from '@/lib/db-mysql' +import { processFullPurchase } from '@/lib/nmi' +import { generateCertificateNumber } from '@/lib/auth-utils' +import { sendWelcomeEmail } from '@/lib/email' export async function POST(request: NextRequest) { try { - const { email, fullName, amount, signupId } = await request.json() + const body = await request.json() + const { + firstName, lastName, email, phone, zip, + cardNumber, cardExp, cardCvv, + paymentToken, + paymentType = 'monthly', + signupId, + } = body - if (!email || !fullName || !amount || !signupId) { - return NextResponse.json( - { error: 'Missing required fields' }, - { status: 400 } - ) + if (!email) { + return NextResponse.json({ error: 'Missing email' }, { status: 400 }) } - // Create payment with Maverick API - const paymentResult = await maverickAPI.createPayment({ - amount: Math.round(amount * 100), // Convert to cents - currency: 'USD', + if (!cardNumber && !paymentToken) { + return NextResponse.json({ error: 'Missing payment information' }, { status: 400 }) + } + + // Minimal checkout: name isn't collected. Derive a billing name from the + // email for the processor / records. + const fName = firstName || email.split('@')[0] || 'Guest' + const lName = lastName || 'Member' + + const result = await processFullPurchase({ + paymentToken, + cardNumber, + cardExp, + cardCvv, + firstName: fName, + lastName: lName, email, - fullName, - description: 'Professional Certification', - returnUrl: `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/payment/success`, - cancelUrl: `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/payment/cancel`, + phone: phone || '', + zip: zip || '', + paymentType, }) - if (!paymentResult.success) { - console.error('Payment creation failed:', paymentResult.error) - - // Update signup status to failed - await supabase - .from('signups') - .update({ payment_status: 'failed' }) - .eq('id', signupId) - - return NextResponse.json( - { error: paymentResult.error || 'Payment creation failed' }, - { status: 500 } - ) + if (!result.success) { + if (signupId) { + await updateSignup(signupId, { payment_status: 'failed' }) + } + return NextResponse.json({ error: result.error || 'Payment declined' }, { status: 400 }) } - // Create payment record - const { data: paymentData, error: paymentError } = await supabase - .from('payments') - .insert({ + // Record payment + const amount = paymentType === 'monthly' ? 29 : 249 + if (signupId) { + await createPayment({ signup_id: signupId, - payment_method: 'maverick', amount, - currency: 'USD', - status: 'pending', - transaction_id: paymentResult.paymentId, + payment_type: 'initial', + status: 'completed', + transaction_id: result.transactionId, }) - .select() - .single() - if (paymentError) { - console.error('Payment record creation failed:', paymentError) - return NextResponse.json( - { error: 'Failed to create payment record' }, - { status: 500 } - ) - } + const certNumber = generateCertificateNumber() + const certExpires = new Date(Date.now() + 18 * 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] - // Update signup with payment ID - await supabase - .from('signups') - .update({ - payment_id: paymentResult.paymentId, - payment_status: 'processing' + await updateSignup(signupId, { + payment_status: 'active', + certificate_number: certNumber, + certificate_expires: certExpires, + ...(result.subscriptionId && { subscription_id: result.subscriptionId }), }) - .eq('id', signupId) + + // Create affiliate referral if applicable + await createAffiliateReferral(signupId) + + // Send welcome email with certificate + try { + await sendWelcomeEmail(email, `${fName} ${lName}`, certNumber, paymentType, amount) + } catch (emailErr) { console.error('Welcome email failed:', emailErr) } + } return NextResponse.json({ success: true, - paymentId: paymentResult.paymentId, - checkoutUrl: paymentResult.checkoutUrl, + transactionId: result.transactionId, + subscriptionId: result.subscriptionId, + paymentType, }) - } catch (error) { - console.error('Payment creation error:', error) - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ) + console.error('Payment error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } -} \ No newline at end of file +} diff --git a/src/app/api/signup/route.ts b/src/app/api/signup/route.ts index 302107b..0b2da8b 100644 --- a/src/app/api/signup/route.ts +++ b/src/app/api/signup/route.ts @@ -1,70 +1,67 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabase } from '@/lib/supabase' +import pool, { createSignup, confirmEarlyLead } from '@/lib/db-mysql' +import { sendAdminNotify } from '@/lib/email' export async function POST(request: NextRequest) { try { - const { email, full_name, phone, amount, monthly_payment, payment_plan_months } = await request.json() + const { email, full_name, phone, amount, monthly_payment, payment_plan_months, + source_lp, referral_code, utm_source, utm_medium, utm_campaign } = await request.json() - if (!email || !full_name || !phone || !amount) { - return NextResponse.json( - { error: 'Missing required fields' }, - { status: 400 } - ) + if (!email || !amount) { + return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }) } + // Minimal checkout may not collect a name — derive one from the email. + const fullName = full_name || email.split('@')[0] - // Check if email already exists - const { data: existingSignup } = await supabase - .from('signups') - .select('*') - .eq('email', email) - .single() + // Do not overwrite an already-paid signup. If a row with this email exists + // and its payment_status is active or completed, short-circuit and return + // the existing row instead of letting createSignup reset it to pending. + const [existingRows] = await pool.execute( + 'SELECT id, email, full_name, phone, amount, payment_status FROM signups WHERE email = ?', + [email] + ) as any[] - if (existingSignup) { - return NextResponse.json( - { error: 'Email already registered' }, - { status: 409 } - ) + if (existingRows.length > 0) { + const status = existingRows[0].payment_status + if (status === 'active' || status === 'completed') { + return NextResponse.json({ + id: existingRows[0].id, + email: existingRows[0].email, + full_name: existingRows[0].full_name, + phone: existingRows[0].phone, + amount: existingRows[0].amount, + already_active: true, + }) + } + // status is 'pending', NULL, or some other non-finalized state — fall + // through to createSignup which will update the existing row. } - // Create signup record - const { data, error } = await supabase - .from('signups') - .insert({ - email, - full_name, - phone, - amount, - monthly_payment: monthly_payment || 39, - payment_plan_months: payment_plan_months || 18, - payment_status: 'pending', - }) - .select() - .single() + const { data, error } = await createSignup({ + email, full_name: fullName, phone: phone || '', amount, + monthly_payment: monthly_payment || 29, + payment_plan_months: payment_plan_months || 10, + source_lp, referral_code, utm_source, utm_medium, utm_campaign, + }) - if (error) { - console.error('Supabase error:', error) - return NextResponse.json( - { error: 'Failed to create signup' }, - { status: 500 } - ) - } + if (error) return NextResponse.json({ error }, { status: 409 }) - return NextResponse.json({ - id: data.id, - email: data.email, - full_name: data.full_name, - phone: data.phone, - amount: data.amount, - monthly_payment: data.monthly_payment, - payment_plan_months: data.payment_plan_months, - payment_status: data.payment_status - }) + try { await confirmEarlyLead(email.trim().toLowerCase()) } catch {} + // Notify the team of the new signup (fire-and-forget) + sendAdminNotify('New signup — hi2b', { + Email: email, Name: fullName, Phone: phone, + Amount: `$${amount}`, + Plan: payment_plan_months > 1 ? `$${monthly_payment}/mo × ${payment_plan_months}` : 'one-time', + 'Source LP': source_lp, 'UTM Source': utm_source, Referral: referral_code, + }).catch(err => console.error('adminNotify signup:', err)) + + return NextResponse.json({ + id: data.id, email: data.email, full_name: data.full_name, + phone: data.phone, amount: data.amount, + }) } catch (error) { console.error('Signup error:', error) - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } -} \ No newline at end of file +} diff --git a/src/app/api/track/lead/route.ts b/src/app/api/track/lead/route.ts new file mode 100644 index 0000000..64fd6c1 --- /dev/null +++ b/src/app/api/track/lead/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from 'next/server' +import { upsertEarlyLead, createEbookLead } from '@/lib/db-mysql' + +const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + +function clientIp(request: NextRequest): string | null { + const fwd = request.headers.get('x-forwarded-for') + if (fwd) return fwd.split(',')[0].trim() + return request.headers.get('x-real-ip') || request.headers.get('cf-connecting-ip') || null +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json().catch(() => ({})) + const { email, phone, name, source_lp, referral_code, utm_source, utm_medium, utm_campaign } = body + + if (!email || typeof email !== 'string' || !EMAIL_RE.test(email)) { + return NextResponse.json({ captured: false, error: 'Invalid email' }, { status: 400 }) + } + + const cleanEmail = email.trim().toLowerCase() + const cleanName = typeof name === 'string' && name.trim() ? name.trim() : undefined + + // Write to early_leads (pre-submit tracking) AND ebook_leads (idempotent) so + // the email shows in the admin portal the instant it's entered. + await Promise.all([ + upsertEarlyLead({ + email: cleanEmail, + phone: typeof phone === 'string' && phone.trim() ? phone.trim() : null, + name: cleanName || null, + ip_address: clientIp(request), + source_lp: source_lp || null, + referral_code: referral_code || null, + utm_source: utm_source || null, + utm_medium: utm_medium || null, + utm_campaign: utm_campaign || null, + }), + createEbookLead({ + email: cleanEmail, + name: cleanName, + source_lp: source_lp || 'form-entry', + utm_source, utm_medium, utm_campaign, + }), + ]) + + return NextResponse.json({ captured: true }) + } catch (error) { + console.error('Lead capture error:', error) + return NextResponse.json({ captured: false, error: 'Internal error' }, { status: 500 }) + } +} diff --git a/src/app/api/track/pageview/route.ts b/src/app/api/track/pageview/route.ts new file mode 100644 index 0000000..9c8c67b --- /dev/null +++ b/src/app/api/track/pageview/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/db-mysql' +import crypto from 'crypto' + +export async function POST(request: NextRequest) { + try { + const { page_slug, referral_code, utm_source, utm_medium, utm_campaign } = await request.json() + const ip = request.headers.get('x-forwarded-for')?.split(',')[0] || 'unknown' + const ipHash = crypto.createHash('sha256').update(ip).digest('hex').substring(0, 16) + + await pool.execute( + `INSERT INTO page_views (page_slug, referral_code, utm_source, utm_medium, utm_campaign, ip_hash) VALUES (?, ?, ?, ?, ?, ?)`, + [page_slug || null, referral_code || null, utm_source || null, utm_medium || null, utm_campaign || null, ipHash] + ) + + return NextResponse.json({ ok: true }) + } catch { + return NextResponse.json({ ok: true }) // Never fail pageview tracking + } +} diff --git a/src/app/api/verify/route.ts b/src/app/api/verify/route.ts new file mode 100644 index 0000000..4cd562e --- /dev/null +++ b/src/app/api/verify/route.ts @@ -0,0 +1,32 @@ +import { NextRequest, NextResponse } from 'next/server' +import { confirmEarlyLead } from '@/lib/db-mysql' +import { checkVerifyToken } from '@/lib/verify-token' + +/** + * Double opt-in verification. The free-guide email links here. + * Validates the signed token, flips the lead to confirmed=1 (active), + * then redirects to the PDF guide so the user gets it immediately. + */ + +const APP_URL = process.env.NEXT_PUBLIC_APP_URL || 'https://hi2b.com' +const PDF_URL = '/ebooks/budget-luxury-travel.pdf' + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url) + const email = (searchParams.get('e') || '').trim().toLowerCase() + const token = searchParams.get('t') || '' + + if (!email || !token || !checkVerifyToken(email, token)) { + return NextResponse.redirect(`${APP_URL}/?verify=invalid`) + } + + // Mark the lead active (verified). Don't fail the redirect on a DB hiccup. + try { + await confirmEarlyLead(email) + } catch (err) { + console.error('verify confirmEarlyLead:', err) + } + + // Deliver the guide. + return NextResponse.redirect(`${APP_URL}${PDF_URL}`) +} diff --git a/src/app/api/webhooks/nmi/route.ts b/src/app/api/webhooks/nmi/route.ts new file mode 100644 index 0000000..6a2abe1 --- /dev/null +++ b/src/app/api/webhooks/nmi/route.ts @@ -0,0 +1,137 @@ +import { NextRequest, NextResponse } from 'next/server' +import { createHmac, timingSafeEqual } from 'crypto' +import pool, { createPayment, updateSignup } from '@/lib/db-mysql' + +/** + * NMI Webhook Handler + * Configure in NMI/Maverick Dashboard: + * Settings → Webhooks → URL: https://hi2b.com/api/webhooks/nmi + * Events: recurring.subscription.add, recurring.subscription.update, recurring.subscription.delete + * Signing key → same value as NMI_WEBHOOK_SECRET env var on the server. + * + * Every request MUST carry one of: + * - X-Signature: sha256= + * - X-NMI-Signature: + * Unsigned requests are rejected with 401. + */ + +function verifySignature(rawBody: string, headerValue: string | null, secret: string): boolean { + if (!headerValue) return false + const provided = headerValue.replace(/^sha256=/i, '').trim().toLowerCase() + if (!/^[0-9a-f]+$/.test(provided)) return false + const expected = createHmac('sha256', secret).update(rawBody).digest('hex') + const a = Buffer.from(provided, 'hex') + const b = Buffer.from(expected, 'hex') + if (a.length !== b.length) return false + return timingSafeEqual(a, b) +} + +export async function POST(request: NextRequest) { + const secret = process.env.NMI_WEBHOOK_SECRET + if (!secret) { + console.error('[NMI Webhook] NMI_WEBHOOK_SECRET not configured; rejecting') + return NextResponse.json({ error: 'Webhook not configured' }, { status: 503 }) + } + + const rawBody = await request.text() + const sigHeader = request.headers.get('x-signature') || request.headers.get('x-nmi-signature') + if (!verifySignature(rawBody, sigHeader, secret)) { + console.warn('[NMI Webhook] Invalid signature — rejecting') + return NextResponse.json({ error: 'Invalid signature' }, { status: 401 }) + } + + let body: any + try { + body = JSON.parse(rawBody) + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) + } + + try { + const { event_id, event_type, event_body } = body + console.log(`[NMI Webhook] ${event_type} | event_id: ${event_id}`) + + if (!event_type || !event_body) { + return NextResponse.json({ message: 'Invalid webhook payload' }, { status: 400 }) + } + + const subscriptionId = event_body.subscription_id + const completedPayments = parseInt(event_body.completed_payments || '0') + const remainingPayments = event_body.remaining_payments + const planAmount = parseFloat(event_body.plan?.amount || '29.00') + + let signupId: number | null = null + const [subMatch] = await pool.execute( + `SELECT id FROM signups WHERE subscription_id = ? LIMIT 1`, + [subscriptionId] + ) as any[] + if (subMatch.length > 0) signupId = subMatch[0].id + + switch (event_type) { + case 'recurring.subscription.add': { + console.log(`[NMI Webhook] Subscription created: ${subscriptionId}`) + break + } + + case 'recurring.subscription.update': { + console.log(`[NMI Webhook] Subscription updated: ${subscriptionId} | Completed: ${completedPayments} | Remaining: ${remainingPayments}`) + if (signupId) { + const [existing] = await pool.execute( + `SELECT id FROM payments WHERE signup_id = ? AND payment_type = 'monthly' AND amount = ? AND created_at > DATE_SUB(NOW(), INTERVAL 1 DAY)`, + [signupId, planAmount] + ) as any[] + + if (existing.length === 0) { + await createPayment({ + signup_id: signupId, + amount: planAmount, + payment_type: 'monthly', + status: 'completed', + transaction_id: `sub_${subscriptionId}_${completedPayments}`, + }) + console.log(`[NMI Webhook] Recorded monthly payment #${completedPayments} for signup ${signupId}`) + } + + if (remainingPayments === '0' || remainingPayments === 0) { + await updateSignup(signupId, { payment_status: 'completed' }) + console.log(`[NMI Webhook] Subscription completed for signup ${signupId}`) + } + } else { + console.warn(`[NMI Webhook] Could not match subscription ${subscriptionId} to a signup`) + console.warn(`[NMI Webhook] Orphan payment: sub=${subscriptionId}, amount=${planAmount}, completed=${completedPayments}`) + } + break + } + + case 'recurring.subscription.delete': { + console.log(`[NMI Webhook] Subscription deleted: ${subscriptionId}`) + if (signupId) { + await updateSignup(signupId, { payment_status: 'cancelled' }) + console.log(`[NMI Webhook] Marked signup ${signupId} as cancelled`) + } + break + } + + default: + console.log(`[NMI Webhook] Unhandled event type: ${event_type}`) + } + + return NextResponse.json({ message: 'Webhook received successfully' }) + } catch (error) { + console.error('[NMI Webhook] Error:', error) + return NextResponse.json({ message: 'Webhook received with errors' }) + } +} + +export async function GET() { + return NextResponse.json({ + status: 'ok', + endpoint: 'NMI Recurring Payment Webhook', + note: 'Signed requests only (HMAC-SHA256 of body, X-Signature header).', + events: [ + 'recurring.subscription.add', + 'recurring.subscription.update', + 'recurring.subscription.delete', + ], + }) +} diff --git a/src/app/dashboard/billing/page.tsx b/src/app/dashboard/billing/page.tsx new file mode 100644 index 0000000..16086ca --- /dev/null +++ b/src/app/dashboard/billing/page.tsx @@ -0,0 +1,212 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useRouter } from 'next/navigation' +import Link from 'next/link' +import { Plane, ArrowLeft, CheckCircle, Clock, XCircle, CreditCard, DollarSign, Calendar } from 'lucide-react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' + +interface UserData { + id: number + fullName: string + totalAmount: number + monthlyPayment: number + paymentPlanMonths: number + paymentStatus: string +} + +interface PaymentData { + id: number + amount: string + currency: string + payment_type: string + status: string + transaction_id: string + created_at: string +} + +export default function BillingPage() { + const router = useRouter() + const [user, setUser] = useState(null) + const [payments, setPayments] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetch('/api/auth/me') + .then(res => { + if (!res.ok) throw new Error() + return res.json() + }) + .then(data => { + setUser(data.user) + setPayments(data.payments || []) + }) + .catch(() => router.push('/dashboard/login')) + .finally(() => setLoading(false)) + }, [router]) + + if (loading) { + return ( +
+
+
+ ) + } + + if (!user) return null + + const paidAmount = payments.filter(p => p.status === 'completed').reduce((sum, p) => sum + Number(p.amount), 0) + const paidMonths = payments.filter(p => p.status === 'completed').length + const remainingMonths = Math.max(0, user.paymentPlanMonths - paidMonths) + const remainingAmount = Number(user.totalAmount) - paidAmount + const progressPercent = (paidAmount / Number(user.totalAmount)) * 100 + + // Generate upcoming payments + const upcomingPayments: { date: string; amount: number }[] = [] + if (remainingMonths > 0) { + const lastPaymentDate = payments.length > 0 + ? new Date(payments[0].created_at) + : new Date() + for (let i = 1; i <= remainingMonths; i++) { + const d = new Date(lastPaymentDate) + d.setMonth(d.getMonth() + i) + upcomingPayments.push({ + date: d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }), + amount: Number(user.monthlyPayment), + }) + } + } + + const statusIcon = (status: string) => { + switch (status) { + case 'completed': return + case 'pending': case 'processing': return + default: return + } + } + + return ( +
+ + +
+ + Back to Dashboard + + +

Billing History

+ + {/* Payment Progress */} + + +
+

Payment Progress

+ {paidMonths} of {user.paymentPlanMonths} payments +
+
+
+
+
+
+

${paidAmount.toFixed(2)}

+

Paid

+
+
+

${remainingAmount.toFixed(2)}

+

Remaining

+
+
+

${Number(user.totalAmount).toFixed(2)}

+

Total

+
+
+ + + + {/* Payment History */} + + + + Payment History + + + + {payments.length === 0 ? ( +

No payments yet.

+ ) : ( +
+ {payments.map((p) => ( +
+
+ {statusIcon(p.status)} +
+

+ {p.payment_type === 'initial' ? 'Initial Payment' : p.payment_type === 'monthly' ? 'Monthly Payment' : 'Refund'} +

+

+ {new Date(p.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })} +

+
+
+
+

+ {p.payment_type === 'refund' ? '-' : ''}${Number(p.amount).toFixed(2)} +

+

{p.transaction_id}

+
+
+ ))} +
+ )} +
+
+ + {/* Upcoming Payments */} + {upcomingPayments.length > 0 && ( + + + + Upcoming Payments + + + +
+ {upcomingPayments.map((p, i) => ( +
+
+ +

{p.date}

+
+

${p.amount.toFixed(2)}

+
+ ))} +
+
+
+ )} + + {/* Support */} +
+

Questions about billing? Call 888-602-2424

+
+
+
+ ) +} diff --git a/src/app/dashboard/certificate/page.tsx b/src/app/dashboard/certificate/page.tsx new file mode 100644 index 0000000..b05c81f --- /dev/null +++ b/src/app/dashboard/certificate/page.tsx @@ -0,0 +1,248 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useRouter } from 'next/navigation' +import Link from 'next/link' +import { Plane, Award, ArrowLeft, Download, MapPin, Calendar, Users, Star, Phone } from 'lucide-react' +import { Button } from '@/components/ui/button' + +interface UserData { + id: number + email: string + fullName: string + phone: string + destination: string | null + totalAmount: number + monthlyPayment: number + paymentPlanMonths: number + paymentStatus: string + certificateNumber: string | null + certificateExpires: string | null + createdAt: string +} + +export default function CertificatePage() { + const router = useRouter() + const [user, setUser] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetch('/api/auth/me') + .then(res => { + if (!res.ok) throw new Error() + return res.json() + }) + .then(data => setUser(data.user)) + .catch(() => router.push('/dashboard/login')) + .finally(() => setLoading(false)) + }, [router]) + + if (loading) { + return ( +
+
+
+ ) + } + + if (!user) return null + + const expiryDate = user.certificateExpires + ? new Date(user.certificateExpires).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) + : 'TBD' + + const issueDate = new Date(user.createdAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) + + return ( +
+ {/* Nav — hidden on print */} + + +
+ {/* Back link — hidden on print */} +
+ + Back to Dashboard + +
+ + {/* ═══ CERTIFICATE — this is the only thing that prints ═══ */} +
+ {/* Gold border effect */} +
+
+ + {/* Header band */} +
+
+
+ + + +
+

+ VACATION CERTIFICATE +

+

Mexico Paradise Vacations

+
+
+ + {/* Certificate body */} +
+ {/* Presented to */} +
+

This certificate is presented to

+

+ {user.fullName} +

+
+ + {/* Description */} +
+

+ This certifies the bearer is entitled to a 5-Day, 4-Night All-Inclusive Vacation for two guests at a participating luxury resort in Mexico, including all meals, beverages, and resort amenities. +

+
+ + {/* Certificate number and dates */} +
+
+ +

Certificate No.

+

{user.certificateNumber || 'PENDING'}

+
+
+ +

Issued

+

{issueDate}

+
+
+ +

Expires

+

{expiryDate}

+
+
+ + {/* Destinations */} +
+

Valid at participating resorts in

+
+ {['Cancun', 'Cabo San Lucas', 'Riviera Maya', 'Puerto Vallarta'].map(d => ( + + {d} + + ))} +
+
+ + {/* Includes */} +
+

Certificate Includes

+
+ {[ + '5 Days / 4 Nights', + 'All-Inclusive Resort', + 'Unlimited Meals & Drinks', + 'Resort Amenities', + 'Beach & Pool Access', + '2 Guest Capacity', + ].map(item => ( +
+
+ + + +
+ {item} +
+ ))} +
+
+ + {/* ═══ CALL TO BOOK — BIG TOLL FREE NUMBER ═══ */} +
+ +

To Book Your Vacation, Call

+ +

+ 888-602-2424 +

+
+

+ Toll-Free • Mon-Fri 9am-8pm • Sat 10am-4pm EST +

+

+ Have your certificate number ready: {user.certificateNumber || 'PENDING'} +

+
+ + {/* Status bar */} +
+
+
+ + {user.paymentStatus === 'active' ? 'CERTIFICATE ACTIVE' : (user.paymentStatus || 'PENDING').toString().toUpperCase()} + +
+
+ + 2 Guests +
+
+
+ + {/* Footer band */} +
+

+ hi2b.com • 724vacation.com • 888-602-2424 +

+
+
+ + {/* Actions — hidden on print */} +
+ + + + +
+
+ + {/* Print styles */} + +
+ ) +} diff --git a/src/app/dashboard/forgot-password/page.tsx b/src/app/dashboard/forgot-password/page.tsx new file mode 100644 index 0000000..b695fc4 --- /dev/null +++ b/src/app/dashboard/forgot-password/page.tsx @@ -0,0 +1,71 @@ +'use client' + +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Loader2, Plane, Mail, ArrowLeft } from 'lucide-react' +import Link from 'next/link' + +export default function ForgotPasswordPage() { + const [email, setEmail] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [sent, setSent] = useState(false) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + await fetch('/api/auth/forgot-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }), + }) + setSent(true) + setIsLoading(false) + } + + return ( +
+
+
+
+ +
+
+ + + + {sent ? 'Check Your Email' : 'Reset Password'} + + + {sent ? ( +
+ +

If an account with {email} exists, we've sent a password reset link.

+ + + +
+ ) : ( +
+
+ + setEmail(e.target.value)} className="h-11 bg-white" required /> +
+ +
+ Back to login +
+
+ )} +
+
+
+
+ ) +} diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx new file mode 100644 index 0000000..642e33c --- /dev/null +++ b/src/app/dashboard/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'Dashboard — Mexico Paradise Vacations', + description: 'View your vacation certificate and billing history.', +} + +export default function DashboardLayout({ children }: { children: React.ReactNode }) { + return <>{children} +} diff --git a/src/app/dashboard/login/page.tsx b/src/app/dashboard/login/page.tsx new file mode 100644 index 0000000..271f54b --- /dev/null +++ b/src/app/dashboard/login/page.tsx @@ -0,0 +1,159 @@ +'use client' + +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' +import { Loader2, Plane, Eye, EyeOff } from 'lucide-react' +import Link from 'next/link' + +export default function LoginPage() { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [showPassword, setShowPassword] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState('') + const [mode, setMode] = useState<'login' | 'register'>('login') + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError('') + setIsLoading(true) + + try { + const endpoint = mode === 'login' ? '/api/auth/login' : '/api/auth/register' + const res = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }), + }) + + const data = await res.json() + if (!res.ok) { + setError(data.error || 'Something went wrong') + return + } + + window.location.href = '/dashboard' + } catch { + setError('Network error. Please try again.') + } finally { + setIsLoading(false) + } + } + + return ( +
+
+ {/* Logo */} +
+
+ +
+

Mexico Paradise Vacations

+

Client Dashboard

+
+ + + + + {mode === 'login' ? 'Welcome Back' : 'Set Up Your Account'} + + + {mode === 'login' + ? 'Log in to view your certificate and billing' + : 'Create a password for your account'} + + + + {/* Tab toggle */} +
+ + +
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(e.target.value)} + className="h-11 bg-white" + required + /> +
+
+ +
+ setPassword(e.target.value)} + className="h-11 bg-white pr-10" + required + minLength={6} + /> + +
+
+ + +
+ + {mode === 'login' && ( +
+ + Forgot your password? + +
+ )} +
+
+ +

+ Back to Home +

+
+
+ ) +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..cba298e --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,210 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useRouter } from 'next/navigation' +import Link from 'next/link' +import { Plane, CreditCard, Award, LogOut, User, Calendar, DollarSign, Shield, MapPin } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Card, CardContent } from '@/components/ui/card' + +interface UserData { + id: number + email: string + fullName: string + phone: string + destination: string | null + totalAmount: number + monthlyPayment: number + paymentPlanMonths: number + paymentStatus: string + certificateNumber: string | null + certificateExpires: string | null + createdAt: string +} + +interface PaymentData { + id: number + amount: number + currency: string + payment_type: string + status: string + transaction_id: string + created_at: string +} + +export default function DashboardPage() { + const router = useRouter() + const [user, setUser] = useState(null) + const [payments, setPayments] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetch('/api/auth/me') + .then(res => { + if (!res.ok) throw new Error('Not authenticated') + return res.json() + }) + .then(data => { + setUser(data.user) + setPayments(data.payments || []) + }) + .catch(() => router.push('/dashboard/login')) + .finally(() => setLoading(false)) + }, [router]) + + const handleLogout = () => { + document.cookie = 'auth_token=; path=/; max-age=0' + router.push('/dashboard/login') + } + + if (loading) { + return ( +
+
+
+ ) + } + + if (!user) return null + + const paidAmount = payments.filter(p => p.status === 'completed').reduce((sum, p) => sum + Number(p.amount), 0) + const totalAmount = Number(user.totalAmount) || 0 + const remainingAmount = totalAmount - paidAmount + const paidMonths = payments.filter(p => p.status === 'completed').length + const remainingMonths = (user.paymentPlanMonths || 0) - paidMonths + const firstName = (user.fullName || user.email || 'Traveler').split(' ')[0] + const paymentStatus = (user.paymentStatus || 'pending').toString() + + return ( +
+ {/* Top Nav */} + + +
+ {/* Welcome */} +
+

Welcome back, {firstName}!

+

Here's your vacation certificate overview.

+
+ + {/* Status Cards */} +
+ + +
+
+ +
+
+

Status

+

+ {paymentStatus.toUpperCase()} +

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

Paid

+

${paidAmount.toFixed(2)}

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

Remaining

+

{remainingMonths} payments

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

Certificate

+

{user.certificateNumber || 'Pending'}

+
+
+
+
+
+ + {/* Quick Links */} +
+ + + +
+ +
+
+

View Certificate

+

See your vacation certificate with details and expiration date

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

Billing History

+

View all payments, upcoming charges, and invoices

+
+
+
+ +
+ + {/* Account Info */} + + +

Account Details

+
+
Name: {user.fullName || 'Not set'}
+
Email: {user.email}
+
Phone: {user.phone || 'Not set'}
+
Member since: {user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '—'}
+
+
+
+
+
+ ) +} diff --git a/src/app/dashboard/reset-password/page.tsx b/src/app/dashboard/reset-password/page.tsx new file mode 100644 index 0000000..fe0998e --- /dev/null +++ b/src/app/dashboard/reset-password/page.tsx @@ -0,0 +1,88 @@ +'use client' + +import { useState, Suspense } from 'react' +import { useSearchParams } from 'next/navigation' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Loader2, Plane, CheckCircle } from 'lucide-react' +import Link from 'next/link' + +function ResetPasswordForm() { + const searchParams = useSearchParams() + const token = searchParams.get('token') || '' + const [password, setPassword] = useState('') + const [confirm, setConfirm] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [done, setDone] = useState(false) + const [error, setError] = useState('') + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (password !== confirm) { setError('Passwords do not match'); return } + setError('') + setIsLoading(true) + + const res = await fetch('/api/auth/reset-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token, password }), + }) + const data = await res.json() + if (!res.ok) { setError(data.error); setIsLoading(false); return } + setDone(true) + setIsLoading(false) + } + + return ( + + + {done ? 'Password Updated!' : 'Create New Password'} + + + {done ? ( +
+ +

Your password has been updated.

+ + + +
+ ) : ( +
+ {error &&
{error}
} +
+ + setPassword(e.target.value)} className="h-11 bg-white" required minLength={6} /> +
+
+ + setConfirm(e.target.value)} className="h-11 bg-white" required minLength={6} /> +
+ +
+ )} +
+
+ ) +} + +export default function ResetPasswordPage() { + return ( +
+
+
+
+ +
+
+ Loading...
}> + + +
+
+ ) +} diff --git a/src/app/faq/page.tsx b/src/app/faq/page.tsx new file mode 100644 index 0000000..ad2bf6d --- /dev/null +++ b/src/app/faq/page.tsx @@ -0,0 +1,88 @@ +import { Plane } from 'lucide-react' +import Link from 'next/link' +import type { Metadata } from 'next' +import { PAYMENT_CONFIG } from '@/app/lp/_config/types' + +export const metadata: Metadata = { + title: 'FAQ — Mexico Paradise Vacations', + description: 'Common questions about Mexico Paradise Vacations all-inclusive certificates: what is included, how to book, refunds, kids, destinations, payment.', +} + +interface QA { q: string; a: React.ReactNode } + +const FAQ: QA[] = [ + { q: 'What is a vacation certificate?', + a: <>A pre-paid voucher for a 5-day, 4-night all-inclusive stay at one of our partner beachfront resorts in Mexico. After purchase, you pick your travel dates and destination from available inventory; we send you the confirmation. }, + { q: 'What does “all-inclusive” actually include?', + a: <>All meals at the resort restaurants, all drinks (alcoholic and non-alcoholic), pool and beach access, daily activities, evening entertainment, in-room amenities, and 24-hour room service. You do not run a tab — you just enjoy the resort. }, + { q: 'Where can I go?', + a: <>Four destinations: Cancun, Cabo San Lucas, Riviera Maya, and Puerto Vallarta. You pick when you redeem the certificate. }, + { q: 'How much does it cost?', + a: <>${PAYMENT_CONFIG.monthlyPrice}/month for {PAYMENT_CONFIG.totalMonths} months (total ${PAYMENT_CONFIG.totalPrice}), or ${PAYMENT_CONFIG.oneTimePrice} as a single payment. Both include the full trip for two adults plus kids under 12. }, + { q: 'Who's covered? Can I bring my kids?', + a: <>The certificate covers two adults plus children under 12, free. Children 12 and older count as additional adults at the resort's prevailing rate. }, + { q: 'When can I travel? Are dates restricted?', + a: <>You can book any available date within 18 months of purchase. Like any resort, the most popular weeks (Christmas, spring break, July 4) book up fast and may carry a small high-season fee paid at the resort. We'll show available dates when you log into your portal. }, + { q: 'Do I have to attend a timeshare presentation?', + a: <>Our standard certificate may include a brief resort presentation as part of the discounted pricing — this is normal for vacation-certificate offers and we're upfront about it. The presentation is optional to act on; many guests simply attend, decline, and continue enjoying their stay. Details and any exemptions for your specific certificate are listed on your booking confirmation. }, + { q: 'Is the resort I stay at a real five-star resort?', + a: <>Yes — the same beachfront properties listed on Booking, Expedia, and resort sites at full price. We'll show you the resort name and link to its full reviews when you select your dates. }, + { q: 'Why is this so much cheaper than booking direct?', + a: <>Resorts make their margin on food, drinks, and excursions — not the room. Filling rooms in advance through partners like us is more profitable to them than leaving rooms empty, so they discount the room rate aggressively. More on how this works. }, + { q: 'Is there a money-back guarantee?', + a: <>Yes — full refund within 30 days of purchase, no questions asked. }, + { q: 'How do I book my trip after I buy?', + a: <>After your first payment you get login credentials to the client portal. Inside, you pick your destination, dates, and resort from available inventory. We confirm by email and you arrive at the resort with your reservation in your name. }, + { q: 'How do I pay?', + a: <>Any major credit or debit card (Visa, Mastercard, AmEx, Discover). Monthly plan: card is charged automatically each month for {PAYMENT_CONFIG.totalMonths} months. One-time plan: charged once at signup. Card information is encrypted and not stored on our servers. }, + { q: 'Can I gift a certificate?', + a: <>Yes. Buy a certificate in your name, then transfer it to the recipient through your client portal. Travel dates can be picked by either of you. }, + { q: 'What if I want to cancel the monthly payments after I've traveled?', + a: <>The monthly plan is a {PAYMENT_CONFIG.totalMonths}-payment commitment for the certificate (total ${PAYMENT_CONFIG.totalPrice}). To avoid the remaining payments, pick the one-time ${PAYMENT_CONFIG.oneTimePrice} option at signup. }, + { q: 'Where can I see real reviews?', + a: <>Our reviews page has testimonials from real travelers. We also share TikTok and Instagram videos of travelers at our resorts on the landing pages. }, + { q: 'Who do I contact with questions?', + a: <>Email support@hi2b.com or call 888-602-2424 during US business hours. }, +] + +export default function FAQPage() { + return ( +
+
+
+ +
+ +
+ Mexico Paradise Vacations + + Back to Home +
+
+ +
+

Frequently Asked Questions

+

Real answers to what people actually ask before booking.

+ +
+ {FAQ.map((item, i) => ( +
+ + + + + +
{item.a}
+
+ ))} +
+ +
+

Ready to claim your certificate?

+ + Get Started — From ${PAYMENT_CONFIG.monthlyPrice}/mo + +
+
+
+ ) +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e9179cf..bc61741 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import Script from "next/script"; import "./globals.css"; import { Toaster } from "@/components/ui/toaster"; @@ -14,24 +15,24 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Z.ai Code Scaffold - AI-Powered Development", - description: "Modern Next.js scaffold optimized for AI-powered development with Z.ai. Built with TypeScript, Tailwind CSS, and shadcn/ui.", - keywords: ["Z.ai", "Next.js", "TypeScript", "Tailwind CSS", "shadcn/ui", "AI development", "React"], - authors: [{ name: "Z.ai Team" }], + title: "Mexico Paradise Vacations — All-Inclusive Vacation Certificates", + description: "5 days, 4 nights all-inclusive Mexico vacation for just $29/month. Cancun, Cabo, Riviera Maya, Puerto Vallarta. 100% money-back guarantee.", + keywords: ["Mexico vacation", "all-inclusive", "Cancun", "Cabo", "vacation certificate", "budget travel"], + authors: [{ name: "Mexico Paradise Vacations" }], icons: { - icon: "https://z-cdn.chatglm.cn/z-ai/static/logo.svg", + icon: "/favicon.svg", }, openGraph: { - title: "Z.ai Code Scaffold", - description: "AI-powered development with modern React stack", - url: "https://chat.z.ai", - siteName: "Z.ai", + title: "Mexico Paradise Vacations — $29/mo All-Inclusive", + description: "5 days, 4 nights all-inclusive Mexico vacation. Cancun, Cabo, Riviera Maya, Puerto Vallarta. Book after first payment.", + url: "https://hi2b.com", + siteName: "Mexico Paradise Vacations", type: "website", }, twitter: { card: "summary_large_image", - title: "Z.ai Code Scaffold", - description: "AI-powered development with modern React stack", + title: "Mexico Paradise Vacations — $29/mo All-Inclusive", + description: "5 days, 4 nights all-inclusive Mexico vacation. 100% money-back guarantee.", }, }; @@ -42,6 +43,20 @@ export default function RootLayout({ }>) { return ( + + + diff --git a/src/app/lp/[slug]/page.tsx b/src/app/lp/[slug]/page.tsx new file mode 100644 index 0000000..509d317 --- /dev/null +++ b/src/app/lp/[slug]/page.tsx @@ -0,0 +1,83 @@ +import dynamic from 'next/dynamic' +import { notFound } from 'next/navigation' +import { getLPConfigBySlug, LP_CONFIGS } from '../_config/pages' + +// Dynamic imports for each LP component +const LP_COMPONENTS: Record> = { + LP01GoldenHour: dynamic(() => import('@/components/lp/pages/LP01GoldenHour')), + LP02MidnightTropical: dynamic(() => import('@/components/lp/pages/LP02MidnightTropical')), + LP03PassportStamp: dynamic(() => import('@/components/lp/pages/LP03PassportStamp')), + LP04CrystalClear: dynamic(() => import('@/components/lp/pages/LP04CrystalClear')), + LP05Fiesta: dynamic(() => import('@/components/lp/pages/LP05Fiesta')), + LP06TheCloser: dynamic(() => import('@/components/lp/pages/LP06TheCloser')), + LP07ResortPreview: dynamic(() => import('@/components/lp/pages/LP07ResortPreview')), + LP08SplitDecision: dynamic(() => import('@/components/lp/pages/LP08SplitDecision')), + LP09Calculator: dynamic(() => import('@/components/lp/pages/LP09Calculator')), + LP10Countdown: dynamic(() => import('@/components/lp/pages/LP10Countdown')), + LP11TheGuide: dynamic(() => import('@/components/lp/pages/LP11TheGuide')), + LP12Dreamboard: dynamic(() => import('@/components/lp/pages/LP12Dreamboard')), + LP13QuizFunnel: dynamic(() => import('@/components/lp/pages/LP13QuizFunnel')), + LP14SocialWall: dynamic(() => import('@/components/lp/pages/LP14SocialWall')), + LP15SavingsJournal: dynamic(() => import('@/components/lp/pages/LP15SavingsJournal')), + LP16CouplesRetreat: dynamic(() => import('@/components/lp/pages/LP16CouplesRetreat')), + LP17Postcards: dynamic(() => import('@/components/lp/pages/LP17Postcards')), + LP18StressRelief: dynamic(() => import('@/components/lp/pages/LP18StressRelief')), + LP19FoodieParadise: dynamic(() => import('@/components/lp/pages/LP19FoodieParadise')), + LP20FamilyEscape: dynamic(() => import('@/components/lp/pages/LP20FamilyEscape')), + // V2 Pages (21-40) + LP21LastChance: dynamic(() => import('@/components/lp/pages/LP21LastChance')), + LP22TheProof: dynamic(() => import('@/components/lp/pages/LP22TheProof')), + LP23VIPAccess: dynamic(() => import('@/components/lp/pages/LP23VIPAccess')), + LP24OneTap: dynamic(() => import('@/components/lp/pages/LP24OneTap')), + LP25FOMOFeed: dynamic(() => import('@/components/lp/pages/LP25FOMOFeed')), + LP26PriceLock: dynamic(() => import('@/components/lp/pages/LP26PriceLock')), + LP27BeforeAfter: dynamic(() => import('@/components/lp/pages/LP27BeforeAfter')), + LP28RiskFree: dynamic(() => import('@/components/lp/pages/LP28RiskFree')), + LP29SpeedDeal: dynamic(() => import('@/components/lp/pages/LP29SpeedDeal')), + LP30Influencer: dynamic(() => import('@/components/lp/pages/LP30Influencer')), + LP31BucketList: dynamic(() => import('@/components/lp/pages/LP31BucketList')), + LP32DealBreaker: dynamic(() => import('@/components/lp/pages/LP32DealBreaker')), + LP33EscapePlan: dynamic(() => import('@/components/lp/pages/LP33EscapePlan')), + LP34TikTokVibes: dynamic(() => import('@/components/lp/pages/LP34TikTokVibes')), + LP35NoBrainer: dynamic(() => import('@/components/lp/pages/LP35NoBrainer')), + LP36WeekendEscape: dynamic(() => import('@/components/lp/pages/LP36WeekendEscape')), + LP37TrustFall: dynamic(() => import('@/components/lp/pages/LP37TrustFall')), + LP38Sunrise: dynamic(() => import('@/components/lp/pages/LP38Sunrise')), + LP39Adrenaline: dynamic(() => import('@/components/lp/pages/LP39Adrenaline')), + LP40GoldenTicket: dynamic(() => import('@/components/lp/pages/LP40GoldenTicket')), + // V3 Pages (41-42) + LP41SeatReserved: dynamic(() => import('@/components/lp/pages/LP41SeatReserved')), + LP42VIPPass: dynamic(() => import('@/components/lp/pages/LP42VIPPass')), + // V4 (43) + LP43RealTraveler: dynamic(() => import('@/components/lp/pages/LP43RealTraveler')), +} + +export function generateStaticParams() { + const params: { slug: string }[] = [] + for (const lp of LP_CONFIGS) { + params.push({ slug: lp.slug }) + params.push({ slug: lp.id.toString() }) + } + return params +} + +export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params + const config = getLPConfigBySlug(slug) + if (!config) return {} + return { + title: config.metadata.title, + description: config.metadata.description, + } +} + +export default async function LPPage({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params + const config = getLPConfigBySlug(slug) + if (!config) notFound() + + const Component = LP_COMPONENTS[config.component] + if (!Component) notFound() + + return +} diff --git a/src/app/lp/_config/fonts.ts b/src/app/lp/_config/fonts.ts new file mode 100644 index 0000000..6ea0d8b --- /dev/null +++ b/src/app/lp/_config/fonts.ts @@ -0,0 +1,132 @@ +import { + Playfair_Display, + Source_Sans_3, + Outfit, + Inter, + Libre_Baskerville, + Lato, + Caveat, + DM_Sans, + Archivo_Black, + Nunito, + Oswald, + Roboto, + Cormorant_Garamond, + Raleway, + Space_Grotesk, + Work_Sans, + IBM_Plex_Sans, + Bebas_Neue, + Barlow, + Merriweather, + Open_Sans, + Poppins, + Quicksand, + Manrope, + Bitter, + Lora, + Karla, + Josefin_Sans, + Nunito_Sans, + Dancing_Script, + Crimson_Pro, + Jost, + Fira_Sans, + Lexend, +} from 'next/font/google' + +// LP1: Golden Hour +export const playfairDisplay = Playfair_Display({ subsets: ['latin'], variable: '--font-playfair', display: 'swap' }) +export const sourceSans3 = Source_Sans_3({ subsets: ['latin'], variable: '--font-source-sans', display: 'swap' }) + +// LP2: Midnight Tropical +export const outfit = Outfit({ subsets: ['latin'], variable: '--font-outfit', display: 'swap' }) +export const inter = Inter({ subsets: ['latin'], variable: '--font-inter', display: 'swap' }) + +// LP3: Passport Stamp +export const libreBaskerville = Libre_Baskerville({ subsets: ['latin'], weight: ['400', '700'], variable: '--font-libre', display: 'swap' }) +export const lato = Lato({ subsets: ['latin'], weight: ['400', '700'], variable: '--font-lato', display: 'swap' }) +export const caveat = Caveat({ subsets: ['latin'], variable: '--font-caveat', display: 'swap' }) + +// LP4: Crystal Clear +export const dmSans = DM_Sans({ subsets: ['latin'], variable: '--font-dm-sans', display: 'swap' }) + +// LP5: Fiesta +export const archivoBlack = Archivo_Black({ subsets: ['latin'], weight: '400', variable: '--font-archivo', display: 'swap' }) +export const nunito = Nunito({ subsets: ['latin'], variable: '--font-nunito', display: 'swap' }) + +// LP6: The Closer +export const oswald = Oswald({ subsets: ['latin'], variable: '--font-oswald', display: 'swap' }) +export const roboto = Roboto({ subsets: ['latin'], variable: '--font-roboto', display: 'swap' }) + +// LP7: Resort Preview +export const cormorantGaramond = Cormorant_Garamond({ subsets: ['latin'], weight: ['400', '600', '700'], variable: '--font-cormorant', display: 'swap' }) +export const raleway = Raleway({ subsets: ['latin'], variable: '--font-raleway', display: 'swap' }) + +// LP8: Split Decision +export const spaceGrotesk = Space_Grotesk({ subsets: ['latin'], variable: '--font-space-grotesk', display: 'swap' }) +export const workSans = Work_Sans({ subsets: ['latin'], variable: '--font-work-sans', display: 'swap' }) + +// LP9: Calculator +export const ibmPlexSans = IBM_Plex_Sans({ subsets: ['latin'], weight: ['400', '600', '700'], variable: '--font-ibm-plex', display: 'swap' }) + +// LP10: Countdown +export const bebasNeue = Bebas_Neue({ subsets: ['latin'], weight: '400', variable: '--font-bebas', display: 'swap' }) +export const barlow = Barlow({ subsets: ['latin'], weight: ['400', '600', '700'], variable: '--font-barlow', display: 'swap' }) + +// LP11: The Guide +export const merriweather = Merriweather({ subsets: ['latin'], weight: ['400', '700'], variable: '--font-merriweather', display: 'swap' }) +export const openSans = Open_Sans({ subsets: ['latin'], variable: '--font-open-sans', display: 'swap' }) + +// LP12: Dreamboard +export const poppins = Poppins({ subsets: ['latin'], weight: ['400', '600', '700'], variable: '--font-poppins', display: 'swap' }) +export const quicksand = Quicksand({ subsets: ['latin'], variable: '--font-quicksand', display: 'swap' }) + +// LP13: Quiz Funnel +export const manrope = Manrope({ subsets: ['latin'], variable: '--font-manrope', display: 'swap' }) + +// LP15: Savings Journal +export const bitter = Bitter({ subsets: ['latin'], variable: '--font-bitter', display: 'swap' }) + +// LP16: Couples Retreat +export const lora = Lora({ subsets: ['latin'], variable: '--font-lora', display: 'swap' }) +export const karla = Karla({ subsets: ['latin'], variable: '--font-karla', display: 'swap' }) + +// LP17: Postcards +export const josefinSans = Josefin_Sans({ subsets: ['latin'], variable: '--font-josefin', display: 'swap' }) +export const nunitoSans = Nunito_Sans({ subsets: ['latin'], variable: '--font-nunito-sans', display: 'swap' }) +export const dancingScript = Dancing_Script({ subsets: ['latin'], variable: '--font-dancing', display: 'swap' }) + +// LP18: Stress Relief +export const crimsonPro = Crimson_Pro({ subsets: ['latin'], variable: '--font-crimson', display: 'swap' }) +export const jost = Jost({ subsets: ['latin'], variable: '--font-jost', display: 'swap' }) + +// LP19: Foodie Paradise (reuses playfairDisplay) +export const firaSans = Fira_Sans({ subsets: ['latin'], weight: ['400', '600', '700'], variable: '--font-fira', display: 'swap' }) + +// LP20: Family Escape +export const lexend = Lexend({ subsets: ['latin'], variable: '--font-lexend', display: 'swap' }) + +// Font groupings by LP for the layout to load only needed fonts +export const LP_FONTS: Record = { + 'golden-hour': ['--font-playfair', '--font-source-sans'], + 'midnight-tropical': ['--font-outfit', '--font-inter'], + 'passport-stamp': ['--font-libre', '--font-lato', '--font-caveat'], + 'crystal-clear': ['--font-dm-sans'], + 'fiesta': ['--font-archivo', '--font-nunito'], + 'the-closer': ['--font-oswald', '--font-roboto'], + 'resort-preview': ['--font-cormorant', '--font-raleway'], + 'split-decision': ['--font-space-grotesk', '--font-work-sans'], + 'calculator': ['--font-ibm-plex'], + 'countdown': ['--font-bebas', '--font-barlow'], + 'the-guide': ['--font-merriweather', '--font-open-sans'], + 'dreamboard': ['--font-poppins', '--font-quicksand'], + 'quiz-funnel': ['--font-manrope', '--font-inter'], + 'social-wall': ['--font-dm-sans'], + 'savings-journal': ['--font-bitter', '--font-source-sans'], + 'couples-retreat': ['--font-lora', '--font-karla'], + 'postcards': ['--font-josefin', '--font-nunito-sans', '--font-dancing'], + 'stress-relief': ['--font-crimson', '--font-jost'], + 'foodie-paradise': ['--font-playfair', '--font-fira'], + 'family-escape': ['--font-lexend'], +} diff --git a/src/app/lp/_config/pages.ts b/src/app/lp/_config/pages.ts new file mode 100644 index 0000000..0eb07b7 --- /dev/null +++ b/src/app/lp/_config/pages.ts @@ -0,0 +1,59 @@ +import type { LPConfig } from './types' + +export const LP_CONFIGS: LPConfig[] = [ + { id: 1, slug: 'golden-hour', name: 'Golden Hour', component: 'LP01GoldenHour', ctaFocus: 'pay-now', metadata: { title: 'Golden Hour — Mexico Paradise Vacations', description: 'Escape to paradise. 5 days, 4 nights all-inclusive Mexico vacation for just $29/month.' } }, + { id: 2, slug: 'midnight-tropical', name: 'Midnight Tropical', component: 'LP02MidnightTropical', ctaFocus: 'pay-now', metadata: { title: 'Exclusive Access — Mexico Paradise Vacations', description: 'Limited spots remaining. Claim your all-inclusive Mexico getaway.' } }, + { id: 3, slug: 'passport-stamp', name: 'Passport Stamp', component: 'LP03PassportStamp', ctaFocus: 'pay-now', metadata: { title: 'Your Next Stamp — Mexico Paradise Vacations', description: 'Adventure awaits. All-inclusive Mexico vacation certificates from $29/month.' } }, + { id: 4, slug: 'crystal-clear', name: 'Crystal Clear', component: 'LP04CrystalClear', ctaFocus: 'pay-now', metadata: { title: '5 Days. 4 Nights. $29/month. — Mexico Paradise', description: 'All-inclusive Mexico vacation. Simple pricing. Incredible value.' } }, + { id: 5, slug: 'fiesta', name: 'Fiesta', component: 'LP05Fiesta', ctaFocus: 'pay-now', metadata: { title: 'Fiesta! — Mexico Paradise Vacations', description: 'Celebrate life with an all-inclusive Mexico vacation from $29/month.' } }, + { id: 6, slug: 'the-closer', name: 'The Closer', component: 'LP06TheCloser', ctaFocus: 'pay-now', metadata: { title: '$3,000 Value for $1.30/day — Mexico Paradise', description: 'The math doesn\'t lie. All-inclusive Mexico vacation for less than your daily coffee.' } }, + { id: 7, slug: 'resort-preview', name: 'Resort Preview', component: 'LP07ResortPreview', ctaFocus: 'pay-now', metadata: { title: 'Tour Your Paradise — Mexico Paradise Vacations', description: 'Preview luxury resorts in Cancun, Cabo, Riviera Maya & Puerto Vallarta.' } }, + { id: 8, slug: 'split-decision', name: 'Split Decision', component: 'LP08SplitDecision', ctaFocus: 'pay-now', metadata: { title: 'Choose Your Paradise — Mexico Paradise Vacations', description: 'Cancun or Cabo? Pick your dream destination. All-inclusive from $29/month.' } }, + { id: 9, slug: 'calculator', name: 'Calculator', component: 'LP09Calculator', ctaFocus: 'pay-now', metadata: { title: 'The Savings Calculator — Mexico Paradise', description: 'See exactly how much you save vs. booking direct. The math speaks for itself.' } }, + { id: 10, slug: 'countdown', name: 'Countdown', component: 'LP10Countdown', ctaFocus: 'pay-now', metadata: { title: 'Time Is Running Out — Mexico Paradise Vacations', description: 'Limited time offer. Claim your all-inclusive Mexico vacation before it\'s gone.' } }, + { id: 11, slug: 'the-guide', name: 'The Guide', component: 'LP11TheGuide', ctaFocus: 'ebook', metadata: { title: 'Budget Luxury Travel Guide — Mexico Paradise', description: 'Free guide: 5 secrets to luxury Mexico vacations on a budget.' } }, + { id: 12, slug: 'dreamboard', name: 'Dreamboard', component: 'LP12Dreamboard', ctaFocus: 'ebook', metadata: { title: 'Build Your Dream Vacation — Mexico Paradise', description: 'Visualize your perfect Mexico getaway. Get the free planning guide.' } }, + { id: 13, slug: 'quiz-funnel', name: 'Quiz Funnel', component: 'LP13QuizFunnel', ctaFocus: 'ebook', metadata: { title: 'Find Your Perfect Destination — Mexico Paradise', description: 'Take the quiz to find your ideal Mexico vacation destination.' } }, + { id: 14, slug: 'social-wall', name: 'Social Wall', component: 'LP14SocialWall', ctaFocus: 'ebook', metadata: { title: 'Join 2,847 Happy Travelers — Mexico Paradise', description: 'See what real travelers are saying about Mexico Paradise Vacations.' } }, + { id: 15, slug: 'savings-journal', name: 'Savings Journal', component: 'LP15SavingsJournal', ctaFocus: 'ebook', metadata: { title: 'Your Vacation Savings Plan — Mexico Paradise', description: '$1.30/day is less than your latte. Start saving for paradise.' } }, + { id: 16, slug: 'couples-retreat', name: 'Couples Retreat', component: 'LP16CouplesRetreat', ctaFocus: 'ebook', metadata: { title: 'You Both Deserve This — Mexico Paradise', description: 'Plan the romantic Mexico getaway you\'ve been dreaming about.' } }, + { id: 17, slug: 'postcards', name: 'Postcards', component: 'LP17Postcards', ctaFocus: 'ebook', metadata: { title: 'Wish You Were Here — Mexico Paradise Vacations', description: 'Send yourself a postcard from the future. Mexico awaits.' } }, + { id: 18, slug: 'stress-relief', name: 'Stress Relief', component: 'LP18StressRelief', ctaFocus: 'ebook', metadata: { title: 'Your Mind Needs a Beach — Mexico Paradise', description: 'Escape the stress. All-inclusive Mexico vacation for your wellbeing.' } }, + { id: 19, slug: 'foodie-paradise', name: 'Foodie Paradise', component: 'LP19FoodieParadise', ctaFocus: 'ebook', metadata: { title: 'Unlimited Everything — Mexico Paradise', description: 'All-inclusive dining at world-class Mexico resorts. From $29/month.' } }, + { id: 20, slug: 'family-escape', name: 'Family Escape', component: 'LP20FamilyEscape', ctaFocus: 'ebook', metadata: { title: 'Give Them the Vacation They Deserve — Mexico Paradise', description: 'Family-friendly all-inclusive Mexico vacations from $29/month.' } }, + // V2 Landing Pages (21-40) — improved with urgency psychology, TikTok embeds, stronger CTAs + { id: 21, slug: 'last-chance', name: 'Last Chance', component: 'LP21LastChance', ctaFocus: 'pay-now', metadata: { title: 'LAST CHANCE — Lock In $29/mo Before Midnight', description: 'This price disappears in minutes. All-inclusive Mexico vacation.' } }, + { id: 22, slug: 'proof', name: 'The Proof', component: 'LP22TheProof', ctaFocus: 'pay-now', metadata: { title: '2,847 Happy Travelers Can\'t Be Wrong', description: 'Watch real TikTok videos from travelers at our resorts.' } }, + { id: 23, slug: 'vip-access', name: 'VIP Access', component: 'LP23VIPAccess', ctaFocus: 'pay-now', metadata: { title: 'VIP ACCESS — Invitation Only Pricing', description: 'You\'ve been selected for exclusive resort pricing. Don\'t let this expire.' } }, + { id: 24, slug: 'one-tap', name: 'One Tap', component: 'LP24OneTap', ctaFocus: 'pay-now', metadata: { title: 'One Tap Away From Paradise', description: 'The simplest way to book your dream Mexico vacation. $29/mo.' } }, + { id: 25, slug: 'fomo-feed', name: 'FOMO Feed', component: 'LP25FOMOFeed', ctaFocus: 'pay-now', metadata: { title: 'Everyone\'s Going to Mexico — Why Aren\'t You?', description: 'See what you\'re missing. Real travelers, real paradise, real cheap.' } }, + { id: 26, slug: 'price-lock', name: 'Price Lock', component: 'LP26PriceLock', ctaFocus: 'pay-now', metadata: { title: 'PRICE LOCK — $29/mo Guaranteed for 30 Minutes', description: 'After this timer expires, the price goes up. Lock it in now.' } }, + { id: 27, slug: 'before-after', name: 'Before & After', component: 'LP27BeforeAfter', ctaFocus: 'pay-now', metadata: { title: 'Your Life Before & After Mexico', description: 'See the transformation. Desk to beach in one payment.' } }, + { id: 28, slug: 'risk-free', name: 'Risk Free', component: 'LP28RiskFree', ctaFocus: 'pay-now', metadata: { title: 'Zero Risk, All Reward — 30-Day Money-Back', description: 'Try it risk-free. If you\'re not amazed, get every penny back.' } }, + { id: 29, slug: 'speed-deal', name: 'Speed Deal', component: 'LP29SpeedDeal', ctaFocus: 'pay-now', metadata: { title: '⚡ FLASH DEAL — 30 Minutes Only', description: 'This deal self-destructs. All-inclusive Mexico from $1.30/day.' } }, + { id: 30, slug: 'influencer', name: 'Influencer', component: 'LP30Influencer', ctaFocus: 'pay-now', metadata: { title: 'As Seen on TikTok — Mexico Paradise', description: 'The vacation deal going viral. Watch the videos, book the trip.' } }, + { id: 31, slug: 'bucket-list', name: 'Bucket List', component: 'LP31BucketList', ctaFocus: 'ebook', metadata: { title: 'Check Mexico Off Your Bucket List', description: 'Life\'s too short for "someday." Get the free planning guide.' } }, + { id: 32, slug: 'deal-breaker', name: 'Deal Breaker', component: 'LP32DealBreaker', ctaFocus: 'ebook', metadata: { title: 'The Deal That Breaks All Other Deals', description: 'Compare us to any travel site. We win every time.' } }, + { id: 33, slug: 'escape-plan', name: 'Escape Plan', component: 'LP33EscapePlan', ctaFocus: 'ebook', metadata: { title: 'Your Escape Plan Starts Here', description: 'Download your free Mexico vacation planning guide.' } }, + { id: 34, slug: 'tiktok-vibes', name: 'TikTok Vibes', component: 'LP34TikTokVibes', ctaFocus: 'ebook', metadata: { title: 'The TikTok-Famous Mexico Vacation', description: 'See why this deal is going viral. Get the free insider guide.' } }, + { id: 35, slug: 'no-brainer', name: 'No Brainer', component: 'LP35NoBrainer', ctaFocus: 'ebook', metadata: { title: 'This Is a No-Brainer — Here\'s Why', description: '$1.30/day for luxury. We\'ll prove it. Get the free breakdown.' } }, + { id: 36, slug: 'weekend-escape', name: 'Weekend Escape', component: 'LP36WeekendEscape', ctaFocus: 'ebook', metadata: { title: 'Turn Any Week Into Paradise', description: '5 days that will change how you think about vacations.' } }, + { id: 37, slug: 'trust-fall', name: 'Trust Fall', component: 'LP37TrustFall', ctaFocus: 'ebook', metadata: { title: 'Don\'t Trust Us — Trust 2,847 Travelers', description: 'Real reviews, real videos, real people. See for yourself.' } }, + { id: 38, slug: 'sunrise', name: 'Sunrise', component: 'LP38Sunrise', ctaFocus: 'ebook', metadata: { title: 'Wake Up to Paradise — Mexico Awaits', description: 'Imagine waking up to ocean views. Get the free travel guide.' } }, + { id: 39, slug: 'adrenaline', name: 'Adrenaline', component: 'LP39Adrenaline', ctaFocus: 'ebook', metadata: { title: 'Adventure Awaits in Mexico', description: 'Ziplines, cenotes, ruins — plus all-inclusive luxury. From $29/mo.' } }, + { id: 40, slug: 'golden-ticket', name: 'Golden Ticket', component: 'LP40GoldenTicket', ctaFocus: 'ebook', metadata: { title: 'You Found the Golden Ticket', description: 'This exclusive offer won\'t last. Claim your all-inclusive paradise.' } }, + // V3 Landing Pages (41-42) — inspired by challenge-style order pages with transformation stories + tier pricing + { id: 41, slug: 'seat-reserved', name: 'Seat Reserved', component: 'LP41SeatReserved', ctaFocus: 'pay-now', metadata: { title: 'Your Seat Is Reserved — Mexico Paradise Vacations', description: 'Your paradise seat is confirmed. Lock in $29/mo before the countdown ends.' } }, + { id: 42, slug: 'vip-pass', name: 'VIP Pass', component: 'LP42VIPPass', ctaFocus: 'pay-now', metadata: { title: 'VIP Platinum Access — Mexico Paradise Vacations', description: 'Private concierge, lifetime rebooking, guest upgrades. VIP cohort closes at midnight.' } }, + // V4 — Real-traveler UGC video lead, 7-section blueprint + { id: 43, slug: 'real-traveler', name: 'Real Traveler', component: 'LP43RealTraveler', ctaFocus: 'pay-now', metadata: { title: 'She Paid $290 for a 5-Star Mexico Vacation — Watch', description: 'Real traveler · Day 4 in Mexico · Same resort her friends paid $2,800 for. See her 20-second story.' } }, +] + +export function getLPConfigBySlug(slug: string): LPConfig | undefined { + return LP_CONFIGS.find(lp => lp.slug === slug || lp.id.toString() === slug) +} + +export function getRandomLPSlug(): string { + const idx = Math.floor(Math.random() * LP_CONFIGS.length) + return LP_CONFIGS[idx].slug +} diff --git a/src/app/lp/_config/types.ts b/src/app/lp/_config/types.ts new file mode 100644 index 0000000..2bb8307 --- /dev/null +++ b/src/app/lp/_config/types.ts @@ -0,0 +1,155 @@ +export interface LPConfig { + id: number + slug: string + name: string + component: string // path for dynamic import + ctaFocus: 'pay-now' | 'ebook' | 'both' + metadata: { + title: string + description: string + } +} + +export interface FormData { + email: string + fullName: string + phone: string +} + +export interface EbookFormData { + email: string + name?: string + source_lp: string +} + +export interface PaymentConfig { + monthlyPrice: number + totalMonths: number + totalPrice: number + oneTimePrice: number +} + +export const PAYMENT_CONFIG: PaymentConfig = { + monthlyPrice: 29, + totalMonths: 10, + totalPrice: 290, + oneTimePrice: 249, +} + +export interface TestimonialData { + quote: string + name: string + location: string + photo?: string +} + +export const TESTIMONIALS: TestimonialData[] = [ + { + quote: "This was the best vacation we've ever had! The all-inclusive resort in Cancun was amazing, and the price was unbeatable.", + name: "Sarah & Mike", + location: "Chicago, IL", + photo: "/images/cdn/photo-1522529599102-193c0d76b5b6.jpg", + }, + { + quote: "I was skeptical at first, but the process was so simple and the vacation exceeded all our expectations. Cabo is breathtaking!", + name: "Jennifer & Tom", + location: "New York, NY", + photo: "/images/cdn/photo-1494790108377-be9c29b29330.jpg", + }, + { + quote: "We paid less than $400 total for a vacation that would normally cost $3,000+. The resort was 5-star quality!", + name: "Maria G.", + location: "Houston, TX", + photo: "/images/cdn/photo-1438761681033-6461ffad8d80.jpg", + }, + { + quote: "The Riviera Maya resort blew our minds. Crystal clear cenotes, amazing food, and world-class service.", + name: "David & Lisa", + location: "Denver, CO", + photo: "/images/cdn/photo-1472099645785-5658abf4ff4e.jpg", + }, + { + quote: "Puerto Vallarta was paradise. We extended our stay an extra 3 nights because we didn't want to leave!", + name: "Rachel T.", + location: "Phoenix, AZ", + photo: "/images/cdn/photo-1544005313-94ddf0286df2.jpg", + }, + { + quote: "Best money I ever spent. The sunset views from our room in Cabo were worth ten times what we paid.", + name: "James & Patricia", + location: "Miami, FL", + photo: "/images/cdn/photo-1500648767791-00dcc994a43e.jpg", + }, +] + +export interface DestinationData { + name: string + tagline: string + images: string[] +} + +export const DESTINATIONS: DestinationData[] = [ + { + name: "Cancun", + tagline: "Pristine beaches & vibrant nightlife", + images: [ + "/images/cdn/photo-1510097467424-192d713fd8b2.jpg", + "/images/cdn/photo-1552074284-5e88ef1aef18.jpg", + "/images/cdn/photo-1510097467424-192d713fd8b2.jpg", + ], + }, + { + name: "Cabo San Lucas", + tagline: "Dramatic cliffs & luxury resorts", + images: [ + "/images/cdn/photo-1593655600619-a88c11180241.jpg", + "/images/cdn/photo-1580846629083-02669741360a.jpg", + "/images/cdn/photo-1527734055665-8def83921139.jpg", + ], + }, + { + name: "Riviera Maya", + tagline: "Ancient ruins & turquoise waters", + images: [ + "/images/cdn/photo-1581710862235-eb6e05d8783f.jpg", + "/images/cdn/photo-1581710862235-eb6e05d8783f.jpg", + "/images/cdn/photo-1581710862235-eb6e05d8783f.jpg", + ], + }, + { + name: "Puerto Vallarta", + tagline: "Stunning sunsets & rich culture", + images: [ + "/images/cdn/photo-1585793753011-397e6e4668d6.jpg", + "/images/cdn/photo-1575762568427-4b23bf947729.jpg", + "/images/cdn/photo-1585793753011-397e6e4668d6.jpg", + ], + }, +] + +export const FAQ_ITEMS = [ + { + question: "When can I travel?", + answer: "You can book your vacation for any available dates within 18 months of your first payment. Some blackout dates may apply during peak holiday seasons.", + }, + { + question: "What's included in 'all-inclusive'?", + answer: "Your all-inclusive package includes accommodation, all meals, drinks (including alcoholic beverages), resort amenities, and access to beaches and pools.", + }, + { + question: "Can I bring my partner?", + answer: "Yes! Each certificate covers a family of four — 2 adults and 2 kids under 12. Additional guests can be added at a discounted rate.", + }, + { + question: "Is there a catch?", + answer: "This special offer is part of our 'Hour to Paradise' program. To receive this deeply discounted vacation rate, we ask that you attend a 90-minute resort tour and presentation about Vacation ownership benefits. There is no obligation to purchase.", + }, + { + question: "Can I get a refund?", + answer: "Yes, we offer a full refund within 30 days of purchase if you haven't booked your travel dates yet.", + }, + { + question: "Can I extend my stay?", + answer: "Yes, you can extend your stay at the same resort for additional nights at a special discounted rate available only to certificate holders.", + }, +] diff --git a/src/app/lp/layout.tsx b/src/app/lp/layout.tsx new file mode 100644 index 0000000..918c8da --- /dev/null +++ b/src/app/lp/layout.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from 'next' +import FloatingPhoneButton from '@/components/lp/shared/FloatingPhoneButton' +import TopPhoneBar from '@/components/lp/shared/TopPhoneBar' + +export const metadata: Metadata = { + title: 'Mexico Paradise Vacations — All-Inclusive Vacation Certificates', + description: '5 days, 4 nights all-inclusive Mexico vacation for just $29/month. Cancun, Cabo, Riviera Maya, Puerto Vallarta.', +} + +export default function LPLayout({ children }: { children: React.ReactNode }) { + return ( +
+ + {children} + +
+ ) +} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..8bc0127 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,8 @@ +import { redirect } from 'next/navigation' + +// Catch-all 404 handler — any unmatched route (typos, removed pages, old +// links, /lp/, bot probes) redirects to the prime landing +// page instead of showing the default 404, so no ad traffic is lost. +export default function NotFound() { + redirect('/lp/golden-hour') +} diff --git a/src/app/page.random.tsx.bak b/src/app/page.random.tsx.bak new file mode 100644 index 0000000..7015230 --- /dev/null +++ b/src/app/page.random.tsx.bak @@ -0,0 +1,27 @@ +// BACKUP of the original random-redirect homepage (replaced 2026-06-22). +// Root cause of the 2,000-visits / 0-sales funnel break: client-side redirect to +// a RANDOM LP destroyed message-match and bounced TikTok's in-app browser. +// Restore this only if intentionally A/B testing random assignment again. +'use client' + +import { useEffect } from 'react' +import { useRouter } from 'next/navigation' +import { LP_CONFIGS } from '@/app/lp/_config/pages' + +export default function Home() { + const router = useRouter() + + useEffect(() => { + const randomLP = LP_CONFIGS[Math.floor(Math.random() * LP_CONFIGS.length)] + router.replace(`/lp/${randomLP.slug}`) + }, [router]) + + return ( +
+
+
+

Loading your paradise...

+
+
+ ) +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 91a3f48..d93a1e5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,441 +1,23 @@ -'use client' - -import { useState } from 'react' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { Alert, AlertDescription } from '@/components/ui/alert' -import { Loader2, CheckCircle, LogIn, Plane, Calendar, Hotel, Coffee, Utensils, Umbrella, HelpCircle } from 'lucide-react' -import { toast } from 'sonner' -import { LoginModal } from '@/components/login-modal' +// hi2b.com homepage — server-rendered, message-matched. +// Replaces the previous client-side random-redirect (backup in page.random.tsx.bak), +// which broke message-match and bounced TikTok's in-app browser → 2,000 visits / 0 sales. +import type { Metadata } from 'next' +import LP00HonestHour from '@/components/lp/pages/LP00HonestHour' + +export const metadata: Metadata = { + title: 'Mexico in 5 Days for $249 — The Only Catch Is One Honest Hour', + description: + 'All-inclusive Mexico vacation: 5 days, 4 nights, 2 adults + 2 kids (kids free). $249 one-time. The only catch is one ~60-minute resort presentation — no obligation. Travel anytime within 18 months.', + openGraph: { + title: 'Mexico in 5 Days for $249 — One Honest Hour', + description: + '5 days / 4 nights all-inclusive Mexico, kids free, $249 one-time. One honest ~60-min presentation, no obligation. Travel within 18 months.', + url: 'https://hi2b.com', + siteName: 'Mexico Paradise Vacations', + type: 'website', + }, +} export default function Home() { - const [email, setEmail] = useState('') - const [fullName, setFullName] = useState('') - const [phone, setPhone] = useState('') - const [isLoading, setIsLoading] = useState(false) - const [isSuccess, setIsSuccess] = useState(false) - const [isLoginModalOpen, setIsLoginModalOpen] = useState(false) - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - - if (!email || !fullName || !phone) { - toast.error('Please fill in all fields') - return - } - - setIsLoading(true) - - try { - // Create signup record - const signupResponse = await fetch('/api/signup', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - email, - full_name: fullName, - phone, - amount: 702, // Total value: $39/month x 18 months - monthly_payment: 39, - payment_plan_months: 18 - }), - }) - - const signupData = await signupResponse.json() - - if (!signupResponse.ok) { - throw new Error(signupData.error || 'Failed to create signup') - } - - // Create payment for first month - const paymentResponse = await fetch('/api/payment/create', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - email, - fullName, - amount: 39, // First month payment - signupId: signupData.id, - isSubscription: true, - }), - }) - - const paymentData = await paymentResponse.json() - - if (!paymentResponse.ok) { - throw new Error(paymentData.error || 'Failed to create payment') - } - - // Redirect to payment checkout - if (paymentData.checkoutUrl) { - window.location.href = paymentData.checkoutUrl - } else { - throw new Error('No checkout URL provided') - } - - } catch (error) { - console.error('Error:', error) - toast.error(error instanceof Error ? error.message : 'An error occurred') - setIsLoading(false) - } - } - - if (isSuccess) { - return ( -
- - - -

Welcome to Paradise!

-

Check your email for your vacation certificate details.

-
-
-
- ) - } - - return ( -
- {/* Header */} -
-
-
-
-
- -
- Mexico Paradise Vacations -
- -
-
-
- - {/* Hero Section */} -
- {/* Hero Banner */} -
-
-

- Escape to Paradise: 5 Days, 4 Nights All-Inclusive Mexico Vacation for Just $39/Month -

-

- Luxury resorts in Cancun, Cabo, Rivera Maya, or Puerto Vallarta await you and your partner -

- -
-
- -
- {/* Left Content */} -
- {/* What's Included */} - - - Your All-Inclusive Paradise Package Includes: - - -
-
- - 5 Days & 4 Nights Accommodation -
-
- - All Meals & Drinks -
-
- - Premium Resort Amenities -
-
- - Access to Beaches & Pools -
-
-
- - Flexible Booking Dates -
-
-

(18-month payment plan at $39/month = $702 total value)

-

- *This special offer is part of our "Hour to Paradise" program. The discounted rate is available because the resort would like to give you a quick tour of facilities and explain benefits of Vacation ownership (90-minute presentation required). -

-
-
-
- - {/* Destinations */} - - - Choose Your Perfect Mexican Paradise - - -
-
- Cancun -
-

Cancun: Pristine beaches & vibrant nightlife

-
-
-
- Cabo San Lucas -
-

Cabo: Dramatic cliffs & luxury resorts

-
-
-
- Rivera Maya -
-

Rivera Maya: Ancient ruins & turquoise waters

-
-
-
- Puerto Vallarta -
-

Puerto Vallarta: Stunning sunsets & culture

-
-
-
-
-
- - {/* How It Works */} - - - How It Works - - -
-
-
1
-

Sign Up & Pay First $39

-
-
-
2
-

Receive Your Certificate

-
-
-
3
-

Book Your Dream Vacation

-
-
-
-
-
- - {/* Right Content - Signup Form */} -
- - -
- Limited Time Offer -
- - Ready for Your Mexican Paradise? - - - Get your all-inclusive vacation certificate today - -
- -
-
- - setFullName(e.target.value)} - className="h-11" - required - /> -
- -
- - setEmail(e.target.value)} - className="h-11" - required - /> -
- -
- - setPhone(e.target.value)} - className="h-11" - required - /> -
- -
-
$39/month
-
for 18 months
-
- Regular price: $1,500+ - Save over $800! -
-

Book immediately after your first payment

-
- - - -

- By signing up, you agree to our payment plan terms and conditions -
- Secure payment powered by Maverick Payments. -

-
-
-
- - {/* Testimonials */} - - - Happy Travelers Love Our Vacation Certificates - - -
-
"
-

- This was the best vacation we've ever had! The all-inclusive resort in Cancun was amazing, and the price was unbeatable. We're already planning our next trip! -

-

- Sarah & Mike, Chicago

-
-
-
"
-

- I was skeptical at first, but the process was so simple and the vacation exceeded all our expectations. Cabo San Lucas is absolutely breathtaking! -

-

- Jennifer & Tom, New York

-
-
-
-
-
- - {/* FAQ Section */} - - - Frequently Asked Questions - - -
-
- -

When can I travel?

-
-

- You can book your vacation for any available dates within 18 months of your first payment. Some blackout dates may apply during peak holiday seasons. -

-
-
-
- -

What's included in 'all-inclusive'?

-
-

- Your all-inclusive package includes accommodation, all meals, drinks (including alcoholic beverages), resort amenities, and access to beaches and pools. -

-
-
-
- -

Can I extend my stay?

-
-

- Yes, you can extend your stay at the same resort for additional nights at a special discounted rate available only to certificate holders. -

-
-
-
- -

What is the "Hour to Paradise" requirement?

-
-

- This special offer is part of our "Hour to Paradise" program. To receive this deeply discounted vacation rate, we ask that you attend a 90-minute resort tour and presentation about Vacation ownership benefits. There is no obligation to purchase. -

-
-
-
-
- - setIsLoginModalOpen(false)} - /> -
- ) -} \ No newline at end of file + return +} diff --git a/src/app/pay/BenefitsSection.tsx b/src/app/pay/BenefitsSection.tsx new file mode 100644 index 0000000..e2e7415 --- /dev/null +++ b/src/app/pay/BenefitsSection.tsx @@ -0,0 +1,150 @@ +'use client' + +import { Sun, Heart, Brain, Waves, Moon, Smile, Eye, Users, Sparkles, Sandwich, Wind, Coffee } from 'lucide-react' + +interface Bullet { + icon: React.ReactNode + title: string + body: string +} + +const WHAT_YOU_GET: Bullet[] = [ + { icon: , title: '5 days / 4 nights', body: 'in a luxury all-inclusive resort' }, + { icon: , title: 'Family of 4', body: '2 adults + 2 kids under 12 — all covered' }, + { icon: , title: 'Unlimited everything', body: 'food, drinks, premium liquor, swim-up bar' }, + { icon: , title: '4 destinations', body: 'Cancun, Cabo, Riviera Maya, Puerto Vallarta' }, +] + +const FOR_YOUR_BODY: Bullet[] = [ + { icon: , title: 'Vitamin D + circadian reset', body: '5 days of real sunshine resets your sleep and mood chemistry' }, + { icon: , title: 'Ocean swims that don\'t feel like exercise', body: 'salt water, full-body, zero gym energy required' }, + { icon: , title: 'Fresh food, no inflammation', body: 'grilled fish, fresh fruit, real meals — inflammation drops, energy climbs' }, + { icon: , title: 'Sleep without alarms', body: 'no Sunday-night dread. No 6am ping. Just sunrise.' }, +] + +const FOR_YOUR_MIND: Bullet[] = [ + { icon: , title: 'Off the grid', body: 'work email locked in a vault. You\'ll come back sharper than 6 months of "productivity hacks."' }, + { icon: , title: 'Perspective', body: 'looking at the horizon for 5 days makes problems look small. Because they are.' }, + { icon: , title: 'Reconnect', body: 'no phones at dinner. The person across from you remembers what your voice sounds like.' }, + { icon: , title: 'Memories on tap', body: 'the kind you\'ll replay on your worst Tuesdays for the rest of your life' }, +] + +export default function BenefitsSection() { + return ( +
+
+
+

What You Actually Get

+

+ This isn't just a vacation.
+ It's a reset for your body and your mind. +

+

+ Most people don't need another productivity app. They need 5 uninterrupted days where their phone doesn't buzz, the food is fresh, and they remember what their own laugh sounds like. +

+
+ +
+ } + items={WHAT_YOU_GET} + /> + } + items={FOR_YOUR_BODY} + /> + } + items={FOR_YOUR_MIND} + /> +
+ +
+
+

The honest truth

+

+ You're not really buying a vacation.
+ You're buying the version of yourself that comes back from it. +

+

+ Calmer. Lighter. With photos of people you love laughing on a beach.
+ For less than what most people spend on Saturday-night dinners in a month. +

+
+
+
+ ) +} + +interface CardProps { + tone: 'warm' | 'emerald' | 'indigo' + tag: string + title: string + subtitle: string + icon: React.ReactNode + items: Bullet[] +} + +const TONES = { + warm: { + border: 'border-orange-200', + head: 'bg-gradient-to-br from-orange-500 to-amber-500 text-white', + iconBg: 'bg-white/20', + bullet: 'text-orange-600 bg-orange-50', + }, + emerald: { + border: 'border-emerald-200', + head: 'bg-gradient-to-br from-emerald-600 to-teal-600 text-white', + iconBg: 'bg-white/20', + bullet: 'text-emerald-700 bg-emerald-50', + }, + indigo: { + border: 'border-indigo-200', + head: 'bg-gradient-to-br from-indigo-600 to-violet-600 text-white', + iconBg: 'bg-white/20', + bullet: 'text-indigo-700 bg-indigo-50', + }, +} + +function BenefitCard({ tone, tag, title, subtitle, icon, items }: CardProps) { + const t = TONES[tone] + return ( +
+
+
+
+ {icon} +
+ {tag} +
+

{title}

+

{subtitle}

+
+
    + {items.map((b, i) => ( +
  • +
    + {b.icon} +
    +
    +

    {b.title}

    +

    {b.body}

    +
    +
  • + ))} +
+
+ ) +} diff --git a/src/app/pay/PayPageClient.tsx b/src/app/pay/PayPageClient.tsx new file mode 100644 index 0000000..205bcc8 --- /dev/null +++ b/src/app/pay/PayPageClient.tsx @@ -0,0 +1,557 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import { useSearchParams } from 'next/navigation' +import Image from 'next/image' +import { + Loader2, Lock, ShieldCheck, CreditCard, CheckCircle, Star, + Plane, Phone, Mail, Check, Headset, Sparkles, X, Undo2, ArrowRight, +} from 'lucide-react' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Button } from '@/components/ui/button' +import { PAYMENT_CONFIG, TESTIMONIALS } from '@/app/lp/_config/types' +import { useEarlyLead } from '@/hooks/useEarlyLead' +import { useTrackingParams } from '@/hooks/useTrackingParams' +import ShowcaseCarousel from './ShowcaseCarousel' +import BenefitsSection from './BenefitsSection' + +const RECENT_BUYERS = [ + { name: 'Sarah from Chicago, IL', when: '2 minutes ago' }, + { name: 'Michael from Austin, TX', when: '7 minutes ago' }, + { name: 'Jennifer from Tampa, FL', when: '12 minutes ago' }, + { name: 'David from Denver, CO', when: '18 minutes ago' }, + { name: 'Maria from Phoenix, AZ', when: '24 minutes ago' }, + { name: 'James from Miami, FL', when: '31 minutes ago' }, + { name: 'Rachel from Seattle, WA', when: '38 minutes ago' }, +] + +const INCLUDED = [ + '5 Days / 4 Nights', + 'All-Inclusive Resort', + 'Unlimited Meals & Drinks', + 'Premium Accommodations', + '4 Destination Choices', + 'Flexible Travel Dates', +] + +const SUPPORT_PHONE = '888-602-2424' + +function formatCardNumber(value: string) { + const digits = value.replace(/\D/g, '').slice(0, 16) + return digits.replace(/(\d{4})(?=\d)/g, '$1 ') +} +function formatExp(value: string) { + const digits = value.replace(/\D/g, '').slice(0, 4) + if (digits.length >= 3) return `${digits.slice(0, 2)}/${digits.slice(2)}` + return digits +} + +export default function PayPageClient() { + const search = useSearchParams() + const repName = search.get('rep') || '' + const initialPlan = (search.get('plan') === 'one-time' ? 'one-time' : 'monthly') as 'monthly' | 'one-time' + const tracking = useTrackingParams('pay-page') + + const [paymentType, setPaymentType] = useState<'monthly' | 'one-time'>(initialPlan) + const [email, setEmail] = useState('') + const [phone, setPhone] = useState('') + const [firstName, setFirstName] = useState('') + const [lastName, setLastName] = useState('') + const [cardNumber, setCardNumber] = useState('') + const [cardExp, setCardExp] = useState('') + const [cardCvv, setCardCvv] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState<{ cert?: string } | null>(null) + + const [tickerIdx, setTickerIdx] = useState(0) + useEffect(() => { + const t = setInterval(() => setTickerIdx(i => (i + 1) % RECENT_BUYERS.length), 4500) + return () => clearInterval(t) + }, []) + + const amount = paymentType === 'monthly' ? PAYMENT_CONFIG.monthlyPrice : PAYMENT_CONFIG.oneTimePrice + const planLabel = paymentType === 'monthly' + ? `$${PAYMENT_CONFIG.monthlyPrice}/mo × ${PAYMENT_CONFIG.totalMonths}` + : `One-time $${PAYMENT_CONFIG.oneTimePrice}` + + const { captured, capture } = useEarlyLead({ + email, phone, name: `${firstName} ${lastName}`.trim() || undefined, + source_lp: 'pay-page', + referral_code: tracking.ref, + utm_source: tracking.utm_source || (repName ? `rep-${repName}` : 'phone-sales'), + utm_medium: tracking.utm_medium, + utm_campaign: tracking.utm_campaign, + }) + + const ticker = useMemo(() => RECENT_BUYERS[tickerIdx], [tickerIdx]) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError(null) + + if (!email || !phone || !firstName || !lastName || !cardNumber || !cardExp || !cardCvv) { + setError('Please fill in every field — we need this to issue your certificate.') + return + } + + setIsLoading(true) + try { + const signupRes = await fetch('/api/signup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email, + full_name: `${firstName} ${lastName}`, + phone, + amount: paymentType === 'monthly' ? PAYMENT_CONFIG.totalPrice : PAYMENT_CONFIG.oneTimePrice, + monthly_payment: PAYMENT_CONFIG.monthlyPrice, + payment_plan_months: paymentType === 'monthly' ? PAYMENT_CONFIG.totalMonths : 1, + source_lp: 'pay-page', + referral_code: tracking.ref, + utm_source: tracking.utm_source || (repName ? `rep-${repName}` : 'phone-sales'), + utm_medium: tracking.utm_medium, + utm_campaign: tracking.utm_campaign, + }), + }) + const signupData = await signupRes.json() + if (!signupRes.ok) throw new Error(signupData.error || 'Could not create your account.') + + const payRes = await fetch('/api/payment/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + firstName, lastName, email, phone, + cardNumber: cardNumber.replace(/\s/g, ''), + cardExp: cardExp.replace('/', ''), + cardCvv, + paymentType, + signupId: signupData.id, + }), + }) + const payData = await payRes.json() + if (!payRes.ok) { + setError(payData.error || 'Payment was declined. Please check your card details or call us at ' + SUPPORT_PHONE + '.') + return + } + setSuccess({ cert: payData.certificateNumber }) + } catch (err) { + setError(err instanceof Error ? err.message : 'Something went wrong. Please call us at ' + SUPPORT_PHONE + '.') + } finally { + setIsLoading(false) + } + } + + if (success) return + + return ( +
+ {/* Top trust bar */} +
+
+
+ + Secure 256-bit SSL checkout +
+ + + {SUPPORT_PHONE} + +
+
+ + {/* Header */} +
+
+
+ + Mexico Paradise Vacations +
+
+ {[1, 2, 3, 4, 5].map(i => )} + 4.9 + / 2,847 reviews +
+
+
+ + {/* Live ticker */} +
+
+ + + + + + {ticker.name} just secured a vacation certificate — {ticker.when} + +
+
+ + {/* Hero */} +
+ {repName && ( +
+ + Prepared for you by {repName} +
+ )} + {!repName && tracking.ref && ( +
+ + Referred by {tracking.ref} +
+ )} +

+ Complete Your Vacation Booking +

+

+ 5 days, 4 nights, all-inclusive — choose from Cancun, Cabo, Riviera Maya, or Puerto Vallarta. + Your certificate is issued instantly after payment. +

+
+ + {/* Showcase carousel */} + + + {/* Benefits — body + mind */} + + + {/* Two-column layout */} +
+ + {/* LEFT — form */} +
+
+ + {/* Plan toggle */} +
+

Choose your plan

+
+ + +
+
+ + {/* Contact info */} +
+

Your contact info

+
+
+ + setFirstName(e.target.value)} required className="h-11" /> +
+
+ + setLastName(e.target.value)} required className="h-11" /> +
+
+ +
+ setEmail(e.target.value)} onBlur={capture} + required className={`h-11 ${captured ? 'pr-9' : ''}`} /> + {captured && } +
+
+
+ + setPhone(e.target.value)} onBlur={capture} + placeholder="(555) 123-4567" required className="h-11" /> +
+
+
+ + {/* Card details */} +
+
+

Payment information

+
+ + Encrypted & secure +
+
+
+
+ +
+ setCardNumber(formatCardNumber(e.target.value))} + placeholder="1234 5678 9012 3456" inputMode="numeric" maxLength={19} required + className="h-11 font-mono pr-24" /> +
+ +
+
+
+
+
+ + setCardExp(formatExp(e.target.value))} + placeholder="MM/YY" inputMode="numeric" maxLength={5} required className="h-11 font-mono" /> +
+
+ + setCardCvv(e.target.value.replace(/\D/g, '').slice(0, 4))} + placeholder="123" inputMode="numeric" maxLength={4} required className="h-11 font-mono" /> +
+
+
+
+ + {/* Charge summary + submit */} +
+
+
+ Today's charge + ${amount.toFixed(2)} +
+

{planLabel}

+ {paymentType === 'monthly' && ( +

+ Then ${PAYMENT_CONFIG.monthlyPrice}/mo × {PAYMENT_CONFIG.totalMonths - 1} more. Cancel anytime. +

+ )} +
+ 100% refund within 30 days, no questions asked +
+
+ + {error && ( +
+ +
+

Payment couldn't go through

+

{error}

+
+
+ )} + + + +

+ By clicking above you authorize {paymentType === 'monthly' + ? `today's $${PAYMENT_CONFIG.monthlyPrice} payment and ${PAYMENT_CONFIG.totalMonths - 1} future monthly charges of $${PAYMENT_CONFIG.monthlyPrice}.` + : `a one-time charge of $${PAYMENT_CONFIG.oneTimePrice}.`} Cancel anytime. 30-day money-back guarantee. +

+ + {/* Card brand strip */} +
+ We accept + + + + +
+
+
+ + {/* Testimonials */} +
+

What real travelers say

+
+ {[1, 2, 3, 4, 5].map(i => )} + 4.9 from 2,847 verified reviews +
+
+ {TESTIMONIALS.slice(0, 3).map((t, i) => ( +
+
+ {t.photo && ( + {t.name} + )} +
+

{t.name}

+

{t.location}

+
+
+
+ {[1, 2, 3, 4, 5].map(i => )} +
+

“{t.quote}”

+
+ ))} +
+
+ + {/* FAQ */} +
+

Frequently asked questions

+
+ + + + +
+
+
+ + {/* RIGHT — sticky sidebar */} + +
+ + {/* Footer */} + +
+ ) +} + +function CardBrand({ label }: { label: string }) { + return ( +
+ {label} +
+ ) +} + +function TrustBadge({ icon, label }: { icon: React.ReactNode; label: string }) { + return ( +
+ {icon} + {label} +
+ ) +} + +function FAQItem({ q, a }: { q: string; a: string }) { + const [open, setOpen] = useState(false) + return ( +
+ + {open &&
{a}
} +
+ ) +} + +function SuccessPanel({ email }: { email: string }) { + return ( +
+
+
+ +
+

You're going to Mexico!

+

+ Your vacation certificate has been activated. We just sent your welcome email and certificate details to: +

+
+

{email}

+
+

+ Check your inbox in the next 1–2 minutes (and your spam folder, just in case). + Your email includes your certificate number and a link to your customer portal where you can choose your destination and book travel dates. +

+ + Go to Customer Portal + +

+ Need help? Call {SUPPORT_PHONE} +

+
+
+ ) +} diff --git a/src/app/pay/ShowcaseCarousel.tsx b/src/app/pay/ShowcaseCarousel.tsx new file mode 100644 index 0000000..d055426 --- /dev/null +++ b/src/app/pay/ShowcaseCarousel.tsx @@ -0,0 +1,139 @@ +'use client' + +import { useCallback, useEffect, useState } from 'react' +import Image from 'next/image' +import useEmblaCarousel from 'embla-carousel-react' +import { ChevronLeft, ChevronRight } from 'lucide-react' + +interface Slide { + src: string + badge: string + headline: string + body: string +} + +const SLIDES: Slide[] = [ + { + src: '/images/showcase/resort-beachfront.jpg', + badge: '5-Star All-Inclusive', + headline: 'World-class beachfront resorts', + body: 'Stay at curated 5-star properties on the Mexican Caribbean and Pacific coasts — chosen for sand, service, and stars.', + }, + { + src: '/images/showcase/infinity-pool-sunset.jpg', + badge: 'Endless Paradise', + headline: 'Watch the sunset from your infinity pool', + body: 'Cocktail in hand, ocean to the horizon. This is what 18 months of "I deserve this" looks like.', + }, + { + src: '/images/showcase/dashboard-screen.jpg', + badge: 'Your Private Dashboard', + headline: 'Login, see everything, book in 2 clicks', + body: 'Manage your certificate, view billing history, choose your dates, and book your resort — all from one beautiful portal.', + }, + { + src: '/images/showcase/certificate-design.jpg', + badge: 'Instant Certificate', + headline: 'Your golden ticket, delivered the moment you pay', + body: 'A unique certificate number is generated and emailed to you the second your payment clears. Frame it, screenshot it — it\'s yours.', + }, + { + src: '/images/showcase/family-vacation-joy.jpg', + badge: 'Whole Family Covered', + headline: 'Bring the family. We\'ll handle the rest.', + body: 'One certificate covers 2 adults and 2 kids under 12 — all-inclusive meals, drinks, and resort amenities for everyone.', + }, +] + +export default function ShowcaseCarousel() { + const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true, align: 'center' }) + const [selected, setSelected] = useState(0) + + useEffect(() => { + if (!emblaApi) return + const onSelect = () => setSelected(emblaApi.selectedScrollSnap()) + emblaApi.on('select', onSelect) + emblaApi.on('reInit', onSelect) + onSelect() + return () => { emblaApi.off('select', onSelect); emblaApi.off('reInit', onSelect) } + }, [emblaApi]) + + useEffect(() => { + if (!emblaApi) return + const t = setInterval(() => emblaApi.scrollNext(), 5500) + return () => clearInterval(t) + }, [emblaApi]) + + const scrollTo = useCallback((idx: number) => emblaApi?.scrollTo(idx), [emblaApi]) + + return ( +
+
+
+

A Peek Inside

+

Login. See your certificate. Book your dates.

+

Here's exactly what you're getting.

+
+ +
+
+
+ {SLIDES.map((s, i) => ( +
+
+ {s.headline} +
+
+
+ + {s.badge} + +

{s.headline}

+

{s.body}

+
+
+
+
+ ))} +
+
+ + + +
+ +
+ {SLIDES.map((_, i) => ( +
+
+
+ ) +} diff --git a/src/app/pay/[code]/page.tsx b/src/app/pay/[code]/page.tsx new file mode 100644 index 0000000..10be69a --- /dev/null +++ b/src/app/pay/[code]/page.tsx @@ -0,0 +1,28 @@ +import { redirect, notFound } from 'next/navigation' +import { getAffiliateByShortCode } from '@/lib/db-mysql' + +interface PageProps { + params: Promise<{ code: string }> + searchParams: Promise> +} + +export default async function PayByShortCode({ params, searchParams }: PageProps) { + const { code } = await params + const sp = await searchParams + + // 2-char shortcuts only; anything else is a typo / bot probe + if (!code || code.length < 2 || code.length > 8) notFound() + + const affiliate = await getAffiliateByShortCode(code).catch(() => null) + if (!affiliate) notFound() + + // Forward any extra params (utm_*, plan, etc.) — only inject ref + const qs = new URLSearchParams() + qs.set('ref', affiliate.referral_code) + for (const [k, v] of Object.entries(sp)) { + if (k === 'ref') continue + if (typeof v === 'string') qs.set(k, v) + else if (Array.isArray(v) && v[0]) qs.set(k, v[0]) + } + redirect(`/pay?${qs.toString()}`) +} diff --git a/src/app/pay/page.tsx b/src/app/pay/page.tsx new file mode 100644 index 0000000..7b5b5e7 --- /dev/null +++ b/src/app/pay/page.tsx @@ -0,0 +1,17 @@ +import type { Metadata } from 'next' +import { Suspense } from 'react' +import PayPageClient from './PayPageClient' + +export const metadata: Metadata = { + title: 'Secure Checkout — Mexico Paradise Vacations', + description: 'Complete your vacation certificate purchase securely. 5 days / 4 nights all-inclusive in Cancun, Cabo, Riviera Maya, or Puerto Vallarta.', + robots: { index: false, follow: false }, +} + +export default function PayPage() { + return ( + }> + + + ) +} diff --git a/src/app/payment/success/page.tsx b/src/app/payment/success/page.tsx index f78fb17..e55e46e 100644 --- a/src/app/payment/success/page.tsx +++ b/src/app/payment/success/page.tsx @@ -1,13 +1,13 @@ 'use client' -import { useEffect, useState } from 'react' +import { useEffect, useState, Suspense } from 'react' import { useSearchParams } from 'next/navigation' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { CheckCircle, Download, Calendar, ExternalLink } from 'lucide-react' import Link from 'next/link' -export default function PaymentSuccess() { +function PaymentSuccessContent() { const searchParams = useSearchParams() const [isLoading, setIsLoading] = useState(true) const [paymentData, setPaymentData] = useState(null) @@ -159,4 +159,19 @@ export default function PaymentSuccess() {
) +} + +export default function PaymentSuccess() { + return ( + +
+
+

Loading...

+
+
+ }> + +
+ ) } \ No newline at end of file diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx new file mode 100644 index 0000000..7b04a62 --- /dev/null +++ b/src/app/privacy/page.tsx @@ -0,0 +1,91 @@ +import { Plane } from 'lucide-react' +import Link from 'next/link' +import type { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'Privacy Policy — Mexico Paradise Vacations', + description: 'Privacy policy for Mexico Paradise Vacations.', +} + +export default function PrivacyPage() { + return ( +
+
+
+ +
+ +
+ Mexico Paradise Vacations + + Back to Home +
+
+ +
+

Privacy Policy

+

Last updated: March 21, 2026

+ +
+
+

Information We Collect

+

We collect information you provide directly when purchasing a vacation certificate or downloading our free travel guide, including: name, email address, phone number, and payment information (processed securely through our payment processor — we do not store credit card numbers on our servers).

+
+ +
+

How We Use Your Information

+
    +
  • To process your vacation certificate purchase and recurring payments
  • +
  • To send your vacation certificate and booking confirmations
  • +
  • To communicate important updates about your vacation booking
  • +
  • To send promotional offers and travel content (you may opt out at any time)
  • +
  • To improve our website and services
  • +
+
+ +
+

Payment Security

+

All payment transactions are processed through NMI, a PCI-DSS Level 1 certified payment gateway. Credit card information is encrypted using 256-bit SSL technology and is never stored on our servers. Recurring payments are managed securely through tokenized payment methods.

+
+ +
+

Information Sharing

+

We do not sell, trade, or rent your personal information to third parties. We may share your information only with:

+
    +
  • Participating resort partners to fulfill your vacation booking
  • +
  • Payment processors to complete transactions
  • +
  • Service providers who assist in operating our website
  • +
  • Law enforcement when required by law
  • +
+
+ +
+

Cookies & Analytics

+

We use Google Analytics to understand how visitors interact with our website. We use cookies to maintain your session and preferences. You may disable cookies in your browser settings, though some features may not function properly.

+
+ +
+

Your Rights

+

You may request access to, correction of, or deletion of your personal data by contacting us at 888-602-2424 or emailing us. We will respond to your request within 30 days.

+
+ +
+

Contact

+

Mexico Paradise Vacations (DBA: 724vacation.com)
+ Toll-Free: 888-602-2424
+ Website: hi2b.com

+
+
+
+ +
+

Mexico Paradise Vacations • 724vacation.com • 888-602-2424

+
+ Terms + Privacy + Home +
+
+
+ ) +} diff --git a/src/app/reviews/page.tsx b/src/app/reviews/page.tsx new file mode 100644 index 0000000..a74c606 --- /dev/null +++ b/src/app/reviews/page.tsx @@ -0,0 +1,82 @@ +import { Plane, Star, MapPin } from 'lucide-react' +import Link from 'next/link' +import Image from 'next/image' +import type { Metadata } from 'next' +import { TESTIMONIALS, PAYMENT_CONFIG } from '@/app/lp/_config/types' + +export const metadata: Metadata = { + title: 'Real Reviews — Mexico Paradise Vacations', + description: 'Read what real travelers say about their Mexico Paradise Vacations all-inclusive trips. Honest reviews from Cancun, Cabo, Riviera Maya, and Puerto Vallarta.', +} + +export default function ReviewsPage() { + const avg = 4.8 + const count = TESTIMONIALS.length + + return ( +
+
+
+ +
+ +
+ Mexico Paradise Vacations + + Back to Home +
+
+ +
+
+

What real travelers say

+
+
+ {[1,2,3,4,5].map(i => ( + + ))} +
+ {avg.toFixed(1)} + · {count}+ reviews from real travelers across our four destinations +
+
+ +
+ {TESTIMONIALS.map((t, i) => ( +
+
+ {t.photo && ( +
+ {t.name} +
+ )} +
+
{t.name}
+
{t.location}
+
+
+
+ {[1,2,3,4,5].map(s => ( + + ))} +
+

“{t.quote}”

+
+ ))} +
+ +
+

Ready to write your own review?

+

5 days, 4 nights, all-inclusive at a real beachfront resort. From ${PAYMENT_CONFIG.monthlyPrice}/mo.

+ + Claim My Certificate + +
+ +

+ Reviews are from real customers and may be lightly edited for length and clarity. Photos used with permission. +

+
+
+ ) +} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts new file mode 100644 index 0000000..d297d4f --- /dev/null +++ b/src/app/sitemap.ts @@ -0,0 +1,26 @@ +import type { MetadataRoute } from 'next' +import { LP_CONFIGS } from './lp/_config/pages' + +const BASE = 'https://hi2b.com' + +export default function sitemap(): MetadataRoute.Sitemap { + const now = new Date() + const staticPages: MetadataRoute.Sitemap = [ + { url: `${BASE}/`, lastModified: now, changeFrequency: 'weekly', priority: 1.0 }, + { url: `${BASE}/pay`, lastModified: now, changeFrequency: 'weekly', priority: 0.9 }, + { url: `${BASE}/about`, lastModified: now, changeFrequency: 'monthly', priority: 0.7 }, + { url: `${BASE}/faq`, lastModified: now, changeFrequency: 'monthly', priority: 0.7 }, + { url: `${BASE}/reviews`, lastModified: now, changeFrequency: 'weekly', priority: 0.7 }, + { url: `${BASE}/affiliate`, lastModified: now, changeFrequency: 'monthly', priority: 0.6 }, + { url: `${BASE}/dashboard`, lastModified: now, changeFrequency: 'monthly', priority: 0.5 }, + { url: `${BASE}/privacy`, lastModified: now, changeFrequency: 'yearly', priority: 0.3 }, + { url: `${BASE}/terms`, lastModified: now, changeFrequency: 'yearly', priority: 0.3 }, + ] + const lpPages: MetadataRoute.Sitemap = LP_CONFIGS.map(lp => ({ + url: `${BASE}/lp/${lp.slug}`, + lastModified: now, + changeFrequency: 'weekly', + priority: 0.8, + })) + return [...staticPages, ...lpPages] +} diff --git a/src/app/terms/page.tsx b/src/app/terms/page.tsx new file mode 100644 index 0000000..8fb94a7 --- /dev/null +++ b/src/app/terms/page.tsx @@ -0,0 +1,229 @@ +import { Plane } from 'lucide-react' +import Link from 'next/link' +import type { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'Terms & Conditions — Mexico Paradise Vacations', + description: 'Terms and conditions for vacation certificate purchases.', +} + +export default function TermsPage() { + return ( +
+ {/* Header */} +
+
+ +
+ +
+ Mexico Paradise Vacations + + Back to Home +
+
+ +
+

Terms & Conditions

+

Last updated: March 21, 2026

+ +
+ +
+

1. Offer Overview

+

Mexico Paradise Vacations ("Company," "we," "us") offers all-inclusive vacation certificate packages at participating luxury resorts in Cancun, Cabo San Lucas, Riviera Maya, and Puerto Vallarta, Mexico. Our promotional vacation certificates provide savings of up to 77% off retail pricing and include accommodations, all meals, drinks, and resort amenities for two (2) adults.

+

By purchasing a vacation certificate, you agree to attend a 90-minute vacation ownership presentation at the resort during your stay. There is absolutely no obligation to purchase anything during or after the presentation.

+
+ +
+

2. Pricing & Payment Plans

+

Vacation certificates are available at the following pricing:

+
    +
  • Monthly Plan: $29.00/month for 10 months ($290.00 total)
  • +
  • One-Time Payment: $249.00
  • +
+

Monthly plan payments are automatically charged to the credit card on file on the same day each month. You may book your vacation dates immediately after your first payment is processed. All prices are in US Dollars (USD).

+
+ +
+

3. 30-Day Money-Back Guarantee

+

We offer a 100% money-back guarantee within thirty (30) days of your initial purchase, provided that:

+
    +
  • You have not booked travel dates for your vacation
  • +
  • You submit a refund request in writing via email or by calling our toll-free number
  • +
  • The refund is processed within 5-10 business days to the original payment method
  • +
+

After the 30-day period, all sales are final. Partial refunds are not available after the guarantee period.

+
+ +
+

4. Cancellation of Recurring Payments

+

You may cancel your monthly payment plan at any time by contacting us at 888-602-2424. Upon cancellation:

+
    +
  • No further payments will be charged to your card
  • +
  • Payments already made are non-refundable (after the 30-day guarantee period)
  • +
  • If the certificate has not been fully paid, it will be inactivated and travel cannot be booked
  • +
  • You may reactivate by paying the remaining balance
  • +
+
+ +
+

5. Eligibility Requirements

+

To qualify for the promotional vacation certificate rate, the following requirements must be met:

+ +

Age Requirements

+
    +
  • Married couples: Both persons must be between the ages of 30-68
  • +
  • Cohabitating couples: Both persons must be between the ages of 30-60
  • +
  • Same-sex married couples: Both persons must be between the ages of 35-60
  • +
+ +

Income Requirements

+
    +
  • Minimum $50,000 USD combined annual household income
  • +
  • Must be employed full-time. Part-time employment does not qualify
  • +
  • Retirees are accepted provided the income requirement is met
  • +
+ +

Marital/Relationship Status

+
    +
  • Offer is valid for married and cohabitating couples only
  • +
  • Cohabitating couples must demonstrate a minimum of 2 years living together
  • +
  • Must tour with spouse, fiancé, or significant other if married, engaged, or cohabitating
  • +
+ +

Credit Card Requirement

+
    +
  • Guests must present a valid major credit card at check-in (Visa, Mastercard, or Discover)
  • +
  • Debit cards, check cards, company cards, and American Express are not accepted
  • +
  • The credit card holder must be the qualifying person
  • +
+ +

Language

+

Both qualified participants must fluently speak, read, and understand either English or Spanish.

+ +

Geographic Restrictions

+

This offer is valid only for permanent residents of the 50 United States and Canada, excluding French Canadian provinces. Residents of or those with family or friends living in the resort destination are not eligible.

+
+ +
+

6. Vacation Ownership Presentation

+

As a condition of the promotional rate, certificate holders must attend a 90-minute vacation ownership presentation at the resort. The following conditions apply:

+
    +
  • The presentation is informational only — there is no obligation to purchase
  • +
  • Both qualified adults must attend the presentation together
  • +
  • Activities and excursions cannot be scheduled on the same day as the presentation
  • +
  • Failure to attend the presentation may result in the promotional rate being voided and the retail room rate being charged
  • +
  • Guests cannot attend any other vacation club presentations during this vacation, including adjoining dates
  • +
+
+ +
+

7. Booking & Travel

+
    +
  • You have 18 months from the date of purchase to use your vacation certificate
  • +
  • To book, call our toll-free number: 888-602-2424
  • +
  • Travel dates are subject to availability at participating resorts
  • +
  • Some blackout dates may apply during peak holiday seasons (Christmas, New Year's, Easter, Spring Break)
  • +
  • Must spend the first night at the designated resort in this promotion
  • +
  • Cannot be used consecutively with any other resort stays or promotional offers
  • +
+
+ +
+

8. Rescheduling Policy

+
    +
  • Once travel dates are chosen, changes are permitted only if received at least 21 days prior to the scheduled arrival date
  • +
  • Changes requested within 21 days of arrival will incur a penalty equal to the total cost of the package
  • +
  • No-shows will forfeit the vacation certificate with no refund
  • +
+
+ +
+

9. Certificate Restrictions

+
    +
  • This promotion is non-transferable
  • +
  • Only one certificate per household, family, or known travel group traveling on the same or similar dates
  • +
  • May not be combined with any other promotional offer, discount code, or special pricing
  • +
  • This is a one-time promotion — not available to guests who have previously used a similar promotional offer at any participating resort
  • +
  • Not valid for existing members of any vacation club, timeshare, or loyalty program at participating resorts
  • +
+
+ +
+

10. What's Included

+

Your all-inclusive vacation certificate covers:

+
    +
  • 5 days and 4 nights of luxury resort accommodation
  • +
  • All meals (breakfast, lunch, dinner) at resort restaurants
  • +
  • Unlimited drinks including alcoholic beverages at resort bars and restaurants
  • +
  • Access to resort pools, beaches, and non-motorized water activities
  • +
  • Use of resort amenities (fitness center, entertainment, common areas)
  • +
  • Accommodation for 2 adults
  • +
+

Not included: Airfare, airport transfers, spa services, motorized water sports, off-site excursions, travel insurance, passport/visa fees, personal expenses, tips/gratuities.

+
+ +
+

11. Travel Documentation

+

All travelers are responsible for ensuring they have valid travel documentation:

+
    +
  • A valid passport with at least 6 months remaining before expiration
  • +
  • Any required visas or travel permits
  • +
  • Travel insurance is strongly recommended but not required
  • +
+
+ +
+

12. Limitation of Liability

+

Mexico Paradise Vacations acts as an intermediary between the customer and the resort. We are not responsible for:

+
    +
  • Resort conditions, services, or quality of accommodations
  • +
  • Flight delays, cancellations, or travel disruptions
  • +
  • Personal injury, loss, or damage during travel
  • +
  • Changes in resort policies, amenities, or availability
  • +
  • Force majeure events including natural disasters, pandemics, or government restrictions
  • +
+
+ +
+

13. Privacy & Data

+

Personal information collected during purchase is used solely for processing your vacation certificate, billing, and customer communications. We do not sell or share your personal data with third parties except as necessary to fulfill your vacation booking. See our Privacy Policy for details.

+
+ +
+

14. Contact Information

+
+

Mexico Paradise Vacations

+

DBA: 724vacation.com

+

Toll-Free: 888-602-2424

+

Website: hi2b.com

+

Hours: Monday-Friday 9am-8pm EST | Saturday 10am-4pm EST

+
+
+ +
+

15. Governing Law

+

These terms and conditions shall be governed by and construed in accordance with the laws of the State of Michigan, United States. Any disputes arising under these terms shall be subject to the exclusive jurisdiction of the courts located in the State of Michigan.

+
+ +
+

16. Acceptance

+

By purchasing a vacation certificate from Mexico Paradise Vacations, you acknowledge that you have read, understood, and agree to be bound by these Terms and Conditions. If you do not agree with any part of these terms, do not purchase a vacation certificate.

+
+ +
+
+ + {/* Footer */} +
+

Mexico Paradise Vacations • 724vacation.com • 888-602-2424

+
+ Terms + Privacy + Home +
+
+
+ ) +} diff --git a/src/components/lp/pages/LP00HonestHour.tsx b/src/components/lp/pages/LP00HonestHour.tsx new file mode 100644 index 0000000..515145b --- /dev/null +++ b/src/components/lp/pages/LP00HonestHour.tsx @@ -0,0 +1,265 @@ +// LP00 "Honest Hour" — the message-matched root landing page (hi2b.com/). +// Built 2026-06-22 to replace the random-redirect homepage. +// +// Design rationale (from funnel diagnosis + competitor research vs Monster +// Reservations / PCK Travel / SandosPromo): +// 1. Server-rendered, ONE page — no random redirect, no spinner. Message +// matches the Sarah UGC ad: $249, 5d/4n, kids free, one honest hour. +// 2. Answer both objections above the fold: WHY it's cheap + WHAT the +// presentation actually is. (The two fears cold TikTok traffic carries.) +// 3. Lead-first: free guide email capture before the $249 ask (PCK's proven +// funnel + HANDOVER fix #3). +// 4. Honesty over fake urgency: NO countdown, NO "2,847 claimed". Uses the +// "travel anytime within 18 months" flexibility framing that the trusted +// competitors use instead. +// 5. $249 one-time shown FIRST (our price edge vs Monster's $299–347). +// +// This is a server component; EbookCaptureForm is the only client island. + +import DirectCheckout from '@/components/lp/shared/DirectCheckout' +import TopPhoneBar from '@/components/lp/shared/TopPhoneBar' +import FloatingPhoneButton from '@/components/lp/shared/FloatingPhoneButton' +import { DESTINATIONS, TESTIMONIALS, FAQ_ITEMS, PAYMENT_CONFIG } from '@/app/lp/_config/types' +import { + Check, ShieldCheck, Clock, Users, Plane, MapPin, Undo2, Star, Sun, Phone, +} from 'lucide-react' + +const SOURCE_LP = 'home' +const ACCENT = '#E8651A' + +const INCLUDES = [ + '5 Days / 4 Nights', + 'All-inclusive resort', + 'Unlimited food & drinks', + '2 adults + 2 kids — kids free', + '4 Mexico destinations', + 'Travel anytime within 18 months', +] + +export default function LP00HonestHour() { + return ( + <> + +
+ {/* ─────────── HERO (SamCart-style 2-column: offer left, checkout right) ─────────── */} +
+
+
+ + {/* LEFT — the offer */} +
+
+ Hour to Paradise — honest travel deals +
+ +

+ 5 days in Mexico from $29/mo. +
The only catch? One honest hour. +

+ +

+ All-inclusive resort, 4 nights, 2 adults + 2 kids — kids free. + Cancún, Cabo, Riviera Maya or Puerto Vallarta. Book any dates in the next + 18 months. No fake countdowns. No high-pressure games. +

+ + {/* Two objections answered immediately */} +
+
+

+ Why is it this cheap? +

+

+ The resort subsidizes your stay in exchange for one ~60–90 minute + tour & ownership presentation. That's the honest trade — you + get a real all-inclusive vacation, they get one hour to show you around. +

+
+
+

+ What's the "hour"? +

+

+ One scheduled resort presentation during your stay. No obligation to + buy anything. A polite "no thank you" is completely fine — + you keep the full vacation either way. +

+
+
+ + {/* Trust row */} +
+ 30-day money-back guarantee + 18-month flexible travel window + 4.8★ from real travelers +
+
+ + {/* RIGHT — pay directly; centered on mobile, 2-col + sticky from tablet up */} + + +
+
+
+ + + {/* ─────────── WHAT'S INCLUDED ─────────── */} +
+

Everything that's included

+
+ {INCLUDES.map((item) => ( +
+ + {item} +
+ ))} +
+
+ + {/* ─────────── DESTINATIONS ─────────── */} +
+
+

Choose your paradise

+

Four all-inclusive Mexico destinations. Same honest deal.

+
+ {DESTINATIONS.map((d) => ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {d.name} +
+

{d.name}

+

{d.tagline}

+
+
+ ))} +
+
+
+ + {/* ─────────── HOW THE HONEST HOUR WORKS ─────────── */} +
+

How the honest hour works

+
+ {[ + { icon: Plane, t: 'Claim your certificate', d: 'Grab the 5-day all-inclusive certificate for $249 (or $29/mo). Pick your dates anytime in the next 18 months.' }, + { icon: Users, t: 'Attend one presentation', d: 'During your stay you attend a single ~60–90 minute resort tour. No obligation. Say no and keep everything.' }, + { icon: Sun, t: 'Enjoy all 4 nights', d: 'The rest of the trip is yours — all-inclusive food, drinks, beaches and pools, with your family.' }, + ].map((s, i) => ( +
+
+ +
+

STEP {i + 1}

+

{s.t}

+

{s.d}

+
+ ))} +
+
+ + {/* ─────────── PRICING (leans $29/mo) ─────────── */} +
+
+

Two honest ways to pay

+

No hidden fees. 30-day money-back guarantee. Travel within 18 months.

+ +
+ {/* Monthly — MOST POPULAR, shown first */} +
+ MOST POPULAR +

Start today

+

${PAYMENT_CONFIG.monthlyPrice}/mo

+

{PAYMENT_CONFIG.totalMonths} months. Cancel anytime.

+

100% refund within 30 days

+
+ {/* One-time — secondary */} +
+

Or pay once

+

${PAYMENT_CONFIG.oneTimePrice}

+

One-time. Save ${PAYMENT_CONFIG.totalPrice - PAYMENT_CONFIG.oneTimePrice} vs. monthly.

+

Same vacation, same guarantee

+
+
+ +
+ + Get my vacation certificate + +

Not ready? Scroll up for the free guide.

+
+
+
+ + {/* ─────────── TESTIMONIALS ─────────── */} +
+

Real travelers, real trips

+
+ {TESTIMONIALS.slice(0, 6).map((t) => ( +
+
+ {Array.from({ length: 5 }).map((_, i) => )} +
+
“{t.quote}”
+
{t.name} · {t.location}
+
+ ))} +
+
+ + {/* ─────────── FAQ (honest disclosure) ─────────── */} +
+
+

Straight answers

+
+ {FAQ_ITEMS.map((f) => ( +
+ + {f.question} + +

{f.answer}

+
+ ))} +
+
+
+ + {/* ─────────── FINAL CTA ─────────── */} +
+
+

Your hour to paradise starts here

+

+ Grab your certificate from just $29/mo, or talk to a real person — your call. +

+ +

+ 30-day money-back guarantee · Travel within 18 months · Polite "no" at the presentation is always fine +

+
+
+ + +
+ + ) +} diff --git a/src/components/lp/pages/LP01GoldenHour.tsx b/src/components/lp/pages/LP01GoldenHour.tsx new file mode 100644 index 0000000..4867f0c --- /dev/null +++ b/src/components/lp/pages/LP01GoldenHour.tsx @@ -0,0 +1,432 @@ +'use client' + +import { useEffect, useState } from 'react' +import { Sun, Utensils, Wine, Waves, Star, MapPin, ArrowRight, Check } from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, DESTINATIONS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const HERO_IMAGE = '/images/cdn/photo-1507525428034-b723cf961d3e.jpg' +const BEACH_IMAGE = '/images/cdn/photo-1519046904884-53103b34b206.jpg' +const RESORT_IMAGE = '/images/cdn/photo-1582719508461-905c673771fd.jpg' +const POOL_IMAGE = '/images/cdn/photo-1571896349842-33c89424de2d.jpg' +const DINNER_IMAGE = '/images/cdn/photo-1414235077428-338989a2e8c0.jpg' + +const DESTINATION_IMAGES: Record = { + 'Cancun': '/images/cdn/photo-1510097467424-192d713fd8b2.jpg', + 'Cabo San Lucas': '/images/cdn/photo-1580415200778-625cb1890ab5.jpg', + 'Riviera Maya': '/images/cdn/photo-1518638150340-f706e86654de.jpg', + 'Puerto Vallarta': '/images/cdn/photo-1585793753011-397e6e4668d6.jpg', +} + +const INCLUDED_ITEMS = [ + { icon: Sun, title: '5 Days & 4 Nights', description: 'Luxurious resort accommodations with ocean views' }, + { icon: Utensils, title: 'All Meals Included', description: 'Breakfast, lunch, dinner at world-class restaurants' }, + { icon: Wine, title: 'Unlimited Drinks', description: 'Premium cocktails, wine, and refreshments all day' }, + { icon: Waves, title: 'Resort Amenities', description: 'Pools, beach access, spa, fitness center, and more' }, +] + +export default function LP01GoldenHour() { + const [scrollY, setScrollY] = useState(0) + + useEffect(() => { + const handleScroll = () => setScrollY(window.scrollY) + window.addEventListener('scroll', handleScroll, { passive: true }) + return () => window.removeEventListener('scroll', handleScroll) + }, []) + + // Gradient deepens as user scrolls + const gradientOpacity = Math.min(0.4, scrollY / 3000) + + return ( +
+ + + {/* Warm golden gradient overlay that deepens on scroll */} +
+ + {/* Top bar */} +
+ Limited Time Offer + | + + remaining +
+ + {/* Hero Section */} +
+
+ Golden sunset over a pristine Mexican beach +
+
+
+ +
+

+ Mexico Paradise Vacations +

+

+ Picture yourself + + in paradise + +

+

+ 5 days and 4 nights at an all-inclusive Mexican resort. + Starting at just ${PAYMENT_CONFIG.monthlyPrice}/month. +

+ +
+
+ + {/* Social proof strip */} +
+
+
+
+
+ {TESTIMONIALS.slice(0, 4).map((t, i) => ( + {t.name} + ))} +
+ + 2,400+ happy travelers + +
+
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} + 4.8/5 + average rating +
+
+
+
+ + {/* What's Included */} +
+
+

+ Everything You Need +

+

+ What's Included +

+

+ Your vacation certificate covers everything for an unforgettable getaway. No hidden fees, no surprises. +

+ +
+ {INCLUDED_ITEMS.map((item) => ( +
+
+ +
+

+ {item.title} +

+

{item.description}

+
+ ))} +
+ + {/* Lifestyle image strip */} +
+ Luxury resort suite + Infinity pool overlooking ocean + Fine dining experience +
+
+
+ + {/* Destinations */} +
+
+

+ Choose Your Paradise +

+

+ Four Stunning Destinations +

+

+ Each destination offers its own unique character. Pick the one that calls to you. +

+ +
+ {DESTINATIONS.map((dest) => ( +
+ {dest.name} +
+
+
+ + Mexico +
+

+ {dest.name} +

+

{dest.tagline}

+
+
+ ))} +
+
+
+ + {/* Full-bleed beach image divider */} +
+ Turquoise ocean with golden sand +
+

+ Your golden hour awaits +

+
+
+ + {/* Pricing + Form Section */} +
+
+

+ Simple, Transparent Pricing +

+

+ Claim Your Certificate +

+

+ Lock in your price today. Travel anytime within 18 months. +

+ +
+ {/* Pricing details */} +
+

+ Your Vacation Certificate Includes +

+
    + {[ + '5 days & 4 nights at a luxury all-inclusive resort', + 'All meals, drinks, and snacks included', + 'Your choice of 4 stunning destinations', + 'Flexible booking — travel within 18 months', + '30-day money-back guarantee', + 'Bring a partner at no extra cost', + ].map((item) => ( +
  • + + {item} +
  • + ))} +
+ +
+
+ $1,500+ + + Save over $1,100 + +
+
+ + ${PAYMENT_CONFIG.monthlyPrice} + + /month for {PAYMENT_CONFIG.totalMonths} months +
+

+ or ${PAYMENT_CONFIG.oneTimePrice} one-time payment +

+
+
+ + {/* Pay Now Form */} +
+

+ Get Started Today +

+

+ Secure your certificate in under 2 minutes +

+ + +
+
+
+
+ + {/* Testimonials */} +
+
+

+ Real Stories +

+

+ What Our Travelers Say +

+ +
+ {TESTIMONIALS.slice(0, 6).map((testimonial, i) => ( + + ))} +
+
+
+ + {/* FAQ */} +
+
+

+ Questions & Answers +

+ + +

+ Frequently Asked Questions +

+ + +
+
+ + {/* Final CTA */} +
+
+

+ Your sunset is waiting +

+

+ Join over 2,400 travelers who have experienced paradise for a fraction of the cost. +

+ + +
+
+ + {/* Footer */} +
+
+

Mexico Paradise Vacations © {new Date().getFullYear()}. All rights reserved.

+

+ Your certificate is valid for 18 months from purchase date. + A 90-minute resort presentation is required to receive the discounted rate. +

+
+
+ + {/* Sticky Mobile CTA */} + +
+ ) +} diff --git a/src/components/lp/pages/LP02MidnightTropical.tsx b/src/components/lp/pages/LP02MidnightTropical.tsx new file mode 100644 index 0000000..cc06800 --- /dev/null +++ b/src/components/lp/pages/LP02MidnightTropical.tsx @@ -0,0 +1,395 @@ +'use client' + +import { useEffect, useState, useRef } from 'react' +import { Sparkles, Shield, Clock, Users, Star, Zap, ArrowRight, Check, Gift } from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import SocialProofTicker from '@/components/lp/shared/SocialProofTicker' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, DESTINATIONS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const HERO_IMAGE = '/images/cdn/photo-1540541338287-41700207dee6.jpg' +const BEACH_NIGHT = '/images/cdn/photo-1507525428034-b723cf961d3e.jpg' + +const DESTINATION_IMAGES: Record = { + 'Cancun': '/images/cdn/photo-1510097467424-192d713fd8b2.jpg', + 'Cabo San Lucas': '/images/cdn/photo-1580415200778-625cb1890ab5.jpg', + 'Riviera Maya': '/images/cdn/photo-1518638150340-f706e86654de.jpg', + 'Puerto Vallarta': '/images/cdn/photo-1585793753011-397e6e4668d6.jpg', +} + +const VIP_PERKS = [ + { icon: Sparkles, text: '5 days & 4 nights — luxury all-inclusive' }, + { icon: Users, text: 'Bring your partner — no extra charge' }, + { icon: Clock, text: '18 months to book your travel dates' }, + { icon: Shield, text: '30-day full money-back guarantee' }, + { icon: Gift, text: 'All meals, drinks & resort amenities included' }, + { icon: Zap, text: 'Instant digital certificate delivery' }, +] + +export default function LP02MidnightTropical() { + const [certificatesLeft, setCertificatesLeft] = useState(23) + const [animateGlow, setAnimateGlow] = useState(false) + const glowRef = useRef(null) + + useEffect(() => { + // Pulse glow effect + glowRef.current = setInterval(() => { + setAnimateGlow(true) + setTimeout(() => setAnimateGlow(false), 1500) + }, 4000) + return () => { + if (glowRef.current) clearInterval(glowRef.current) + } + }, []) + + // Simulate scarcity countdown + useEffect(() => { + const timer = setInterval(() => { + setCertificatesLeft(prev => { + if (prev <= 5) return 23 + return prev - 1 + }) + }, 45000) + return () => clearInterval(timer) + }, []) + + return ( +
+ + + {/* Animated gradient background */} +
+
+
+ + {/* Urgency bar */} +
+
+ + + Only {certificatesLeft} certificates remaining at this price + + | + Offer expires in + +
+
+ + {/* Hero */} +
+
+ Tropical resort at twilight +
+
+ +
+
+ + EXCLUSIVE VIP OFFER +
+ +

+ Your VIP Ticket to{' '} + + Paradise + +

+ +

+ 5 days. 4 nights. All-inclusive luxury in Mexico. +
+ Starting at just{' '} + ${PAYMENT_CONFIG.monthlyPrice}/month. +

+ + + +

+ {certificatesLeft} of 50 certificates remaining this month +

+
+
+ + {/* Glassmorphic feature cards */} +
+
+

+ Everything Included +

+

+ No hidden fees. No surprises. Just paradise. +

+ +
+ {VIP_PERKS.map((perk) => ( +
+
+
+ +
+

{perk.text}

+
+
+ ))} +
+
+
+ + {/* Destinations with glass cards */} +
+
+

+ Choose Your Destination +

+

+ Four world-class Mexican destinations. The choice is yours. +

+ +
+ {DESTINATIONS.map((dest) => ( +
+ {dest.name} +
+
+

+ {dest.name} +

+

{dest.tagline}

+
+ {/* Glow line on hover */} +
+
+ ))} +
+
+
+ + {/* VIP Ticket-style pricing */} +
+
+
+ {/* Ticket header */} +
+
+ + VIP CERTIFICATE +
+

+ Mexico Paradise +

+

5 Days / 4 Nights All-Inclusive

+ + {/* Ticket perforation dots */} +
+
+
+ + {/* Ticket body */} +
+
+
+ $1,500+ + + 73% OFF + +
+
+ + ${PAYMENT_CONFIG.monthlyPrice} + + /mo +
+

+ for {PAYMENT_CONFIG.totalMonths} months · or ${PAYMENT_CONFIG.oneTimePrice} one-time +

+
+ +
    + {[ + 'Luxury all-inclusive resort stay', + 'Choice of 4 destinations', + 'All meals & unlimited drinks', + 'Flexible dates within 18 months', + '30-day money-back guarantee', + ].map((item) => ( +
  • + + {item} +
  • + ))} +
+ + + +
+

+ Only {certificatesLeft} certificates left at this price +

+
+
+
+ + +
+
+ + {/* Testimonials */} +
+
+

+ What VIP Travelers Say +

+ +
+ {TESTIMONIALS.slice(0, 6).map((t, i) => ( + + ))} +
+
+
+ + {/* FAQ */} +
+
+ + +

+ Questions? We've Got Answers +

+ + +
+
+ + {/* Bottom CTA */} +
+
+

+ {certificatesLeft} certificates remaining +

+

+ Don't Miss Out +

+

+ This exclusive VIP rate won't last. Secure your certificate today. +

+ +
+
+ + {/* Footer */} +
+
+

Mexico Paradise Vacations © {new Date().getFullYear()}. All rights reserved.

+

+ Your certificate is valid for 18 months from purchase date. + A 90-minute resort presentation is required to receive the discounted rate. +

+
+
+ + {/* Social Proof Ticker */} + + + {/* Sticky Mobile CTA */} + +
+ ) +} diff --git a/src/components/lp/pages/LP03PassportStamp.tsx b/src/components/lp/pages/LP03PassportStamp.tsx new file mode 100644 index 0000000..a2e8ce8 --- /dev/null +++ b/src/components/lp/pages/LP03PassportStamp.tsx @@ -0,0 +1,511 @@ +'use client' + +import { useState } from 'react' +import { Plane, MapPin, Stamp, Star, Calendar, Shield, Utensils, Palmtree, ArrowRight, Check } from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, DESTINATIONS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const HERO_IMAGE = '/images/cdn/photo-1436491865332-7a61a109db05.jpg' + +const DESTINATION_IMAGES: Record = { + 'Cancun': '/images/cdn/photo-1510097467424-192d713fd8b2.jpg', + 'Cabo San Lucas': '/images/cdn/photo-1580415200778-625cb1890ab5.jpg', + 'Riviera Maya': '/images/cdn/photo-1518638150340-f706e86654de.jpg', + 'Puerto Vallarta': '/images/cdn/photo-1585793753011-397e6e4668d6.jpg', +} + +const STAMP_DATES = ['MAR 2026', 'APR 2026', 'JUN 2026', 'SEP 2026'] + +const JOURNEY_STEPS = [ + { icon: Stamp, title: 'Claim Your Certificate', description: 'Secure your spot with a simple payment plan' }, + { icon: Calendar, title: 'Pick Your Dates', description: 'Choose any available week within 18 months' }, + { icon: MapPin, title: 'Choose Your Destination', description: 'Cancun, Cabo, Riviera Maya, or Puerto Vallarta' }, + { icon: Plane, title: 'Pack Your Bags', description: 'Show up and enjoy — everything else is covered' }, +] + +export default function LP03PassportStamp() { + const [activeDestination, setActiveDestination] = useState(0) + + return ( +
+ + + {/* Paper texture overlay */} +
+ + {/* Top banner */} +
+

+ + Adventure awaits! + + | + Your next passport stamp is just ${PAYMENT_CONFIG.monthlyPrice}/month away +

+
+ + {/* Hero */} +
+
+ Vintage map and travel accessories +
+
+ +
+
+ {/* Vintage airplane doodle */} +
+ +
+
+ +

+ Mexico Paradise Vacations presents... +

+ +

+ Your Next Stamp + Awaits +

+ +

+ 5 days and 4 nights at an all-inclusive Mexican resort. Four breathtaking destinations to choose from. +

+ +

+ Starting at just{' '} + + ${PAYMENT_CONFIG.monthlyPrice}/month + {' '} + for {PAYMENT_CONFIG.totalMonths} months +

+ + +
+
+
+ + {/* Journey steps */} +
+
+

+ It's easier than you think +

+

+ Your Journey in 4 Simple Steps +

+ +
+ {JOURNEY_STEPS.map((step, i) => ( +
+ {/* Step number stamp */} +
+ {i + 1} +
+ +

+ {step.title} +

+

{step.description}

+
+ ))} +
+
+
+ + {/* Passport-style destination cards */} +
+
+

+ Pick your paradise +

+

+ Your Passport Destinations +

+ + {/* Destination tabs */} +
+ {DESTINATIONS.map((dest, i) => ( + + ))} +
+ + {/* Active destination - passport page style */} +
+
+ {/* Passport page header */} +
+ + MEXICO ENTRY STAMP + + + {STAMP_DATES[activeDestination]} + +
+ + {/* Destination image with stamp overlay */} +
+ {DESTINATIONS[activeDestination].name} + {/* Stamp overlay */} +
+ Approved + {STAMP_DATES[activeDestination]} + MEXICO +
+
+ + {/* Destination info */} +
+

+ {DESTINATIONS[activeDestination].name} +

+

{DESTINATIONS[activeDestination].tagline}

+ +
+ {['All-Inclusive Resort', 'Ocean Views', 'Gourmet Dining', 'Beach Access'].map((feature) => ( +
+ + {feature} +
+ ))} +
+ + {/* Handwritten note */} +
+

+ “Can't wait to visit! This is going to be amazing!” +

+
+
+
+
+
+
+ + {/* What's included — journal style */} +
+
+

+ Everything you need for the perfect trip +

+

+ What's In Your Certificate +

+ +
+ {[ + { icon: Palmtree, title: '5 Days & 4 Nights', desc: 'Luxurious resort accommodations with stunning views', note: 'Check-in is a breeze!' }, + { icon: Utensils, title: 'All Meals & Drinks', desc: 'Breakfast, lunch, dinner, and unlimited beverages', note: 'The food is incredible' }, + { icon: MapPin, title: '4 Destinations', desc: 'Cancun, Cabo, Riviera Maya, or Puerto Vallarta', note: 'Hard to choose just one!' }, + { icon: Shield, title: '30-Day Guarantee', desc: 'Full refund if you change your mind within 30 days', note: 'Totally risk-free' }, + ].map((item) => ( +
+ +

+ {item.title} +

+

{item.desc}

+

+ ^ {item.note} +

+
+ ))} +
+
+
+ + {/* Pricing & Form */} +
+
+

+ Ready to go? +

+

+ Claim Your Vacation Certificate +

+ +
+ {/* Pricing card */} +
+
+
+ +
+
+

+ Travel Certificate +

+

5 days / 4 nights all-inclusive

+
+
+ +
+
+ $1,500+ + + Save over $1,100 + +
+
+ + ${PAYMENT_CONFIG.monthlyPrice} + + /month +
+

+ for {PAYMENT_CONFIG.totalMonths} months · or ${PAYMENT_CONFIG.oneTimePrice} one-time +

+
+ +
    + {[ + 'Luxury all-inclusive resort', + 'All meals and unlimited drinks', + 'Your choice of 4 destinations', + 'Bring your partner for free', + '18 months to book your dates', + '30-day money-back guarantee', + ].map((item) => ( +
  • + + {item} +
  • + ))} +
+ +

+ The best decision you'll make this year! +

+
+ + {/* Form */} +
+

+ Start Your Adventure +

+

Fill in your details and we'll get you booked

+ + + + +
+
+
+
+ + {/* Testimonials */} +
+
+

+ From our travelers' journals +

+

+ Traveler Stories +

+ +
+ {TESTIMONIALS.slice(0, 6).map((t, i) => ( + + ))} +
+
+
+ + {/* FAQ */} +
+
+

+ Before you pack... +

+ + +

+ Frequently Asked Questions +

+ +
+ +
+
+
+ + {/* Final CTA */} +
+
+ +

+ The adventure begins now +

+

+ Your passport is waiting for a new stamp! +

+ +
+
+ + {/* Footer */} +
+
+

Mexico Paradise Vacations © {new Date().getFullYear()}. All rights reserved.

+

+ Your certificate is valid for 18 months from purchase date. + A 90-minute resort presentation is required to receive the discounted rate. +

+
+
+ + {/* Sticky Mobile CTA */} + +
+ ) +} diff --git a/src/components/lp/pages/LP04CrystalClear.tsx b/src/components/lp/pages/LP04CrystalClear.tsx new file mode 100644 index 0000000..f1940f4 --- /dev/null +++ b/src/components/lp/pages/LP04CrystalClear.tsx @@ -0,0 +1,187 @@ +'use client' + +import { ArrowRight, Check } from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const BEACH_IMAGE = '/images/cdn/photo-1507525428034-b723cf961d3e.jpg' + +export default function LP04CrystalClear() { + return ( +
+ + + {/* Hero — radically minimal */} +
+
+

+ Mexico Paradise Vacations +

+ +

+ 5 days. 4 nights. +
+ $39/month. +
+ That's it. +

+ +

+ All-inclusive Mexico vacation certificate. + Four destinations. Zero hidden fees. +

+ + +
+
+ + {/* Single beach image — full bleed */} +
+ Crystal clear turquoise water and white sand beach +
+ + {/* Simple value props */} +
+
+
+ {[ + 'Luxury all-inclusive resort stay', + 'Cancun, Cabo, Riviera Maya, or Puerto Vallarta', + 'All meals and unlimited drinks', + 'Bring your partner at no extra cost', + 'Book anytime within 18 months', + '30-day money-back guarantee', + ].map((item) => ( +
+ + {item} +
+ ))} +
+
+
+ + {/* Pricing — dead simple */} +
+
+
+ $1,500+ + + Save $1,100+ + +
+
+ + ${PAYMENT_CONFIG.monthlyPrice} + + /mo +
+

+ for {PAYMENT_CONFIG.totalMonths} months · or ${PAYMENT_CONFIG.oneTimePrice} one-time +

+
+
+ + {/* Email-first form — minimal */} +
+
+

+ Ready? +

+

+ Takes less than 2 minutes. +

+ + + + +
+
+ + {/* One-line testimonial */} +
+
+

+ “{TESTIMONIALS[0].quote}” +

+
+ {TESTIMONIALS[0].photo && ( + {TESTIMONIALS[0].name} + )} + + {TESTIMONIALS[0].name}, {TESTIMONIALS[0].location} + +
+
+
+ + + + {/* Tiny FAQ */} +
+
+

FAQ

+ +
+
+ + {/* Final CTA — ultra minimal */} +
+
+

+ Paradise is waiting. +

+ +
+
+ + {/* Footer */} +
+
+

Mexico Paradise Vacations © {new Date().getFullYear()}

+

+ Certificate valid 18 months. 90-minute resort presentation required. +

+
+
+ + {/* Sticky Mobile CTA */} + +
+ ) +} diff --git a/src/components/lp/pages/LP05Fiesta.tsx b/src/components/lp/pages/LP05Fiesta.tsx new file mode 100644 index 0000000..b447afe --- /dev/null +++ b/src/components/lp/pages/LP05Fiesta.tsx @@ -0,0 +1,538 @@ +'use client' + +import { useState, useRef, useCallback } from 'react' +import { Sun, Utensils, Waves, Palmtree, Music, Star, MapPin, ArrowRight, Check, PartyPopper, Heart, Sparkles } from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, DESTINATIONS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const HERO_IMAGE = '/images/cdn/photo-1518105779142-d975f22f1b0a.jpg' +const BEACH_FIESTA = '/images/cdn/photo-1519046904884-53103b34b206.jpg' + +const DESTINATION_IMAGES: Record = { + 'Cancun': '/images/cdn/photo-1510097467424-192d713fd8b2.jpg', + 'Cabo San Lucas': '/images/cdn/photo-1580415200778-625cb1890ab5.jpg', + 'Riviera Maya': '/images/cdn/photo-1518638150340-f706e86654de.jpg', + 'Puerto Vallarta': '/images/cdn/photo-1585793753011-397e6e4668d6.jpg', +} + +const INCLUDED_FEATURES = [ + { icon: Sun, title: '5 Days & 4 Nights', desc: 'Wake up to ocean views every morning', color: '#FFD600' }, + { icon: Utensils, title: 'All Meals & Drinks', desc: 'Gourmet dining and unlimited beverages', color: '#FF1744' }, + { icon: Waves, title: 'Beach & Pool Access', desc: 'Pristine beaches and sparkling pools', color: '#00BFA5' }, + { icon: Palmtree, title: 'Resort Amenities', desc: 'Spa, fitness center, entertainment', color: '#FFD600' }, + { icon: Heart, title: 'Whole Family Covered', desc: '2 adults + 2 kids under 12', color: '#FF1744' }, + { icon: Music, title: 'Entertainment', desc: 'Live music, shows, and nightlife', color: '#00BFA5' }, +] + +// SVG Papel Picado Banner Component +function PapelPicadoBanner({ className = '' }: { className?: string }) { + const colors = ['#FF1744', '#FFD600', '#00BFA5', '#FF6D00', '#AA00FF', '#FF1744', '#FFD600', '#00BFA5'] + + return ( +
+ + {/* String line */} + + {/* Papel picado flags */} + {colors.map((color, i) => { + const x = i * 150 + const w = 140 + return ( + + {/* Flag body */} + + {/* Decorative cutouts */} + + + + + + {/* Heart cutout */} + + + ) + })} + +
+ ) +} + +// Confetti particle +interface Particle { + id: number + x: number + y: number + color: string + size: number + rotation: number +} + +export default function LP05Fiesta() { + const [confettiParticles, setConfettiParticles] = useState([]) + const confettiIdRef = useRef(0) + + const spawnConfetti = useCallback((e: React.MouseEvent) => { + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect() + const cx = rect.left + rect.width / 2 + const cy = rect.top + rect.height / 2 + const colors = ['#FF1744', '#FFD600', '#00BFA5', '#FF6D00', '#AA00FF', '#2979FF'] + + const newParticles: Particle[] = Array.from({ length: 20 }, () => { + confettiIdRef.current += 1 + return { + id: confettiIdRef.current, + x: cx + (Math.random() - 0.5) * 200, + y: cy + (Math.random() - 0.5) * 150 - 50, + color: colors[Math.floor(Math.random() * colors.length)], + size: 6 + Math.random() * 6, + rotation: Math.random() * 360, + } + }) + + setConfettiParticles(prev => [...prev, ...newParticles]) + + // Clean up after animation + setTimeout(() => { + setConfettiParticles(prev => prev.filter(p => !newParticles.find(np => np.id === p.id))) + }, 1200) + }, []) + + return ( +
+ + + {/* Confetti particles */} +
+ {confettiParticles.map((p) => ( +
0.5 ? '50%' : '2px', + opacity: 0.9, + }} + /> + ))} +
+ + {/* CSS for confetti animation */} + + + {/* Papel Picado Banner */} + + + {/* Hero */} +
+
+ Colorful Mexican beach scene +
+
+ +
+
+ + Mexico Paradise Vacations +
+ +

+ Life is a{' '} + + Fiesta! + + +

+ +

+ 5 days of all-inclusive paradise in Mexico. + Four amazing destinations. Starting at just{' '} + ${PAYMENT_CONFIG.monthlyPrice}/month! +

+ + +
+
+ + {/* Colorful wave divider */} +
+ + + + + +
+ + {/* What's Included */} +
+
+
+

+ Todo Incluido +

+

+ What's Included +

+
+ +
+ {INCLUDED_FEATURES.map((feature) => ( +
+
+ +
+

+ {feature.title} +

+

{feature.desc}

+
+ ))} +
+
+
+ + {/* Destination cards */} +
+ {/* Top papel picado */} + + +
+
+

+ Destinos +

+

+ Choose Your Adventure +

+
+ +
+ {DESTINATIONS.map((dest, i) => { + const borderColors = ['#FF1744', '#FFD600', '#00BFA5', '#FF6D00'] + return ( +
+ {dest.name} +
+
+
+ + Mexico +
+

+ {dest.name} +

+

{dest.tagline}

+
+ {/* Colored corner accent */} +
+
+ ) + })} +
+
+
+ + {/* Beach divider */} +
+ Beautiful beach in Mexico +
+

+ Vamos a la playa! +

+
+
+ + {/* Pricing & Form */} +
+
+
+

+ Precios Increibles +

+

+ Grab Your Certificate +

+
+ +
+ {/* Pricing card — fiesta style */} +
+ {/* Festive header */} +
+ +

+ Fiesta Package +

+

5 Days / 4 Nights All-Inclusive

+
+ +
+
+
+ $1,500+ + + SAVE $1,100+ + +
+
+ + ${PAYMENT_CONFIG.monthlyPrice} + + /mo +
+

+ for {PAYMENT_CONFIG.totalMonths} months · or ${PAYMENT_CONFIG.oneTimePrice} one-time +

+
+ +
    + {[ + 'Luxury all-inclusive resort stay', + 'All meals and unlimited drinks', + 'Choice of 4 destinations', + 'Bring your partner free', + '18 months to book dates', + '30-day money-back guarantee', + ].map((item) => ( +
  • + + {item} +
  • + ))} +
+
+
+ + {/* Form */} +
+

+ Let's Get This Party Started! +

+

+ Secure your certificate and start planning your fiesta +

+ + + + +
+
+
+
+ + {/* Testimonials */} +
+
+
+

+ Testimonios +

+

+ Happy Travelers +

+
+ +
+ {TESTIMONIALS.slice(0, 6).map((t, i) => { + const borderColors = ['#FF1744', '#FFD600', '#00BFA5'] + return ( + + ) + })} +
+
+
+ + {/* FAQ */} +
+
+
+

+ Preguntas Frecuentes +

+ + +

+ Got Questions? +

+
+ +
+ +
+
+
+ + {/* Final CTA */} +
+ +
+ +

+ The fiesta starts now! +

+

+ Don't wait — grab your all-inclusive Mexico vacation certificate today. +

+ +
+
+ + {/* Footer */} +
+
+

Mexico Paradise Vacations © {new Date().getFullYear()}. All rights reserved.

+

+ Your certificate is valid for 18 months from purchase date. + A 90-minute resort presentation is required to receive the discounted rate. +

+
+
+ + {/* Sticky Mobile CTA */} + +
+ ) +} diff --git a/src/components/lp/pages/LP06TheCloser.tsx b/src/components/lp/pages/LP06TheCloser.tsx new file mode 100644 index 0000000..aed4ba7 --- /dev/null +++ b/src/components/lp/pages/LP06TheCloser.tsx @@ -0,0 +1,654 @@ +'use client' + +import { useState, useEffect, useRef } from 'react' +import { Check, ArrowRight, AlertTriangle, Gift, Star, Shield, Zap, Clock, ChevronDown } from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import ComparisonTable from '@/components/lp/shared/ComparisonTable' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, DESTINATIONS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const RED = '#B71C1C' +const YELLOW = '#FFD54F' +const GREEN = '#4CAF50' + +const scrollToForm = () => { + document.getElementById('signup-form')?.scrollIntoView({ behavior: 'smooth' }) +} + +function CTAButton({ text = 'YES! I Want My Vacation Certificate!' }: { text?: string }) { + return ( + + ) +} + +function YellowHighlight({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} + +function SavingsCounter() { + const [scrollPercent, setScrollPercent] = useState(0) + + useEffect(() => { + const handleScroll = () => { + const scrollTop = window.scrollY + const docHeight = document.documentElement.scrollHeight - window.innerHeight + setScrollPercent(Math.min(1, scrollTop / docHeight)) + } + window.addEventListener('scroll', handleScroll, { passive: true }) + return () => window.removeEventListener('scroll', handleScroll) + }, []) + + const savings = Math.floor(scrollPercent * 2601) + + return ( +
+

+ Your savings +

+

+ ${savings.toLocaleString()} +

+
+ ) +} + +function ValueItem({ item, value }: { item: string; value: string }) { + return ( +
+ +
+ {item} +
+ + {value} + +
+ ) +} + +export default function LP06TheCloser() { + return ( +
+ + + + + {/* Attention Bar */} +
+ + WARNING: THIS OFFER EXPIRES TONIGHT AT MIDNIGHT + +
+ + {/* Hero Section */} +
+
+

+ Attention: Anyone who wants an incredible Mexico vacation without the incredible price tag +

+

+ How To Get A 5-Day All-Inclusive Mexico Vacation + For Just $1.30 Per Day +

+

+ (That's less than your morning coffee... for a vacation your friends will think cost you $3,000+) +

+
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} + Rated 4.8/5 by 2,847 happy travelers +
+ Stunning beachfront resort in Mexico + +
+
+ + {/* Problem Section */} +
+
+

+ Let Me Guess... You're TIRED Of Watching Everyone Else + Take Amazing Vacations While You Stay Home? +

+
+

+ You scroll through Instagram and see your friends lounging on pristine beaches, + sipping cocktails at infinity pools, and eating at five-star restaurants... +

+

+ And you think: "Must be nice to afford that." +

+

+ Because every time you look at booking a real vacation, the numbers make your stomach turn. + $3,000... $4,000... $5,000+ for just a few days of paradise. +

+

+ You've tried the budget travel hacks. The "secret" websites. The off-season deals. + And you STILL end up paying way more than you planned. +

+

+ But what if it didn't have to be that way? +

+
+
+
+ + {/* Solution Section */} +
+
+

+ Introducing The Mexico Paradise Vacation Certificate +

+

+ A revolutionary way to experience a 5-day, 4-night all-inclusive Mexico vacation at + a fraction of the cost of traditional booking. +

+
+ {DESTINATIONS.map((dest) => ( +
+ {dest.name} +
+
+

+ {dest.name} +

+

{dest.tagline}

+
+
+ ))} +
+

+ Choose ANY of these world-class destinations. Your certificate, your choice. +

+ +
+
+ + {/* Value Stack Section */} +
+
+

+ Here's EVERYTHING You're Getting Today +

+

+ (When you add it all up, the value is absolutely INSANE) +

+ +
+ + + + + + + + + +
+
+ + Total Retail Value: + + + $3,550 + +
+
+ + YOUR Price Today: + + + Just ${PAYMENT_CONFIG.monthlyPrice}/mo + +
+

+ Or ${PAYMENT_CONFIG.oneTimePrice} one-time payment (best value!) +

+
+
+ +
+

+ That's a $3,000+ value for just $1.30 per day! +

+

+ Less than a cup of coffee. Less than a candy bar. Less than a single song on iTunes. +

+
+ + +
+
+ + {/* Social Proof - Testimonials */} +
+
+

+ Don't Just Take Our Word For It... +

+

+ Real people. Real vacations. Real savings. +

+
+ {TESTIMONIALS.slice(0, 4).map((t, i) => ( + + ))} +
+
+
+ + {/* Comparison Table */} +
+
+

+ See How We Stack Up Against The Competition +

+

+ The difference is crystal clear +

+
+ +
+
+ +
+
+
+ + {/* What If Section */} +
+
+

+ Imagine This... +

+
+

+ Picture yourself 6 months from now... +

+

+ You wake up in a luxurious resort room. The sound of waves crashing on the beach + filters through the open balcony doors. You stretch, smile, and walk out to see the + turquoise Caribbean stretching out before you. +

+

+ You head down to breakfast. Eggs benedict, fresh tropical fruit, bottomless mimosas. + All included. +

+

+ Your afternoon? Maybe the infinity pool. Maybe snorkeling. Maybe a couples massage at the spa. + Whatever you want. +

+

+ And the best part? While everyone around you paid $3,000+ for the same experience, + you paid less than $400. +

+

+ That's the power of the Mexico Paradise Vacation Certificate. +

+
+
+
+ + {/* More Testimonials */} +
+
+

+ Even MORE Happy Travelers... +

+
+ {TESTIMONIALS.slice(4).map((t, i) => ( + + ))} +
+
+
+ + {/* Objection Handling */} +
+
+

+ "But What If..." +

+
+
+

+ "What if I can't travel right away?" +

+

+ No problem! You have 18 full months to book your trip. + Pick the dates that work for YOUR schedule. +

+
+
+

+ "What if I don't like it?" +

+

+ We offer a 100% money-back guarantee within 30 days. + Zero risk. Zero hassle. +

+
+
+

+ "Can I really afford $39/month?" +

+

+ Let's put it this way: that's $1.30 per day. + Skip one latte a week and you're covered. + Can you really afford NOT to take a vacation? +

+
+
+

+ "Is this legit?" +

+

+ We're a verified business with 2,847+ happy customers, + a 4.8/5 star rating, and SSL-encrypted payments. Your money is safe. +

+
+
+
+
+ + {/* Urgency Section */} +
+
+

+ + This Price Won't Last Forever +

+

+ When the timer hits zero, this offer is GONE. + The price goes back to regular retail, and you'll be kicking yourself + for not acting when you had the chance. +

+
+ +
+

+ Only 47 certificates remaining at this price! +

+ +
+
+ + {/* Guarantee Section */} +
+
+ +

+ Our 30-Day Money-Back Guarantee +

+

+ Try the Mexico Paradise Vacation Certificate for a full 30 days. + If you're not completely thrilled with your purchase, + simply contact us and we'll refund every single penny. + No questions asked. No hoops to jump through. + Your satisfaction is 100% guaranteed. +

+
+
+ + {/* Final Breakdown + CTA Section */} +
+
+

+ Let's Do The Math One More Time... +

+
+
+
+ Resort Stay (5 days) + $1,200 +
+
+ All-Inclusive Package + $800 +
+
+ Amenities & Activities + $400 +
+
+ Guest Pass + $500 +
+
+ Extras & Perks + $650 +
+
+
+ Total Value: + $3,550 +
+
+ + You Pay: + +
+ + ${PAYMENT_CONFIG.monthlyPrice}/mo + +

or ${PAYMENT_CONFIG.oneTimePrice} one-time

+
+
+
+
+ +

+ You save over $3,000 with + ZERO risk (30-day guarantee) +

+
+
+ + {/* Signup Form Section */} +
+
+

+ + Claim Your Certificate NOW +

+

+ Fill out the form below to secure your ${PAYMENT_CONFIG.monthlyPrice}/month vacation certificate +

+ +
+ +
+
+
+ + {/* FAQ Section */} + + +
+
+

+ Frequently Asked Questions +

+ +
+
+ + {/* Final Closing */} +
+
+

+ You Have Two Choices Right Now... +

+
+
+

+ Option A: Do Nothing +

+
    +
  • + - + Keep scrolling Instagram jealously +
  • +
  • + - + Pay $3,000+ next time you travel +
  • +
  • + - + Wonder "what if" for the next year +
  • +
+
+
+

+ Option B: Take Action +

+
    +
  • + + Lock in $1.30/day for paradise +
  • +
  • + + Save over $3,000 guaranteed +
  • +
  • + + Be the one YOUR friends envy +
  • +
+
+
+ +

+ 30-day money-back guarantee. SSL encrypted. Cancel anytime. +

+
+
+ + {/* P.S. Section */} +
+
+

+ P.S. — Remember, this special price of ${PAYMENT_CONFIG.monthlyPrice}/month is + only available while the countdown timer is running. Once it hits zero, the price goes up. + There are only 47 certificates left at this price. +

+

+ P.P.S. — Still not sure? You're protected by our 30-day + money-back guarantee. That means you can try this completely risk-free. + If you don't love it, you get every penny back. What do you have to lose? +

+

+ P.P.P.S. — Think about it this way: for the price of a few + fast food meals per month, you could be lounging on a beach in Mexico. + The only question is... will you take action? +

+
+
+ + {/* Footer */} +
+

+ Mexico Paradise Vacations. All rights reserved. +

+

+ This site is not a part of the Facebook or Google websites. Results may vary. +

+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP07ResortPreview.tsx b/src/components/lp/pages/LP07ResortPreview.tsx new file mode 100644 index 0000000..8f0b6fd --- /dev/null +++ b/src/components/lp/pages/LP07ResortPreview.tsx @@ -0,0 +1,446 @@ +'use client' + +import { useState, useRef, useEffect } from 'react' +import { + Play, + Waves, + UtensilsCrossed, + Dumbbell, + Sparkles, + Wine, + Sun, + MapPin, + ChevronLeft, + ChevronRight, + Star, + ArrowRight, +} from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import DestinationCarousel from '@/components/lp/shared/DestinationCarousel' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import PricingDisplay from '@/components/lp/shared/PricingDisplay' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, DESTINATIONS } from '@/app/lp/_config/types' + +const NAVY = '#1A237E' +const GOLD = '#C9B037' +const BEIGE = '#F5F5DC' + +const AMENITIES = [ + { + icon: Waves, + title: 'Infinity Pools', + description: 'Multiple heated pools with ocean views, swim-up bars, and private cabanas.', + image: '/images/cdn/photo-1582719508461-905c673771fd.jpg', + }, + { + icon: UtensilsCrossed, + title: 'World-Class Dining', + description: 'From authentic Mexican cuisine to international fine dining, all included.', + image: '/images/cdn/photo-1414235077428-338989a2e8c0.jpg', + }, + { + icon: Sparkles, + title: 'Luxury Spa', + description: 'Full-service spa with traditional temazcal, massages, and beauty treatments.', + image: '/images/cdn/photo-1544161515-4ab6ce6db874.jpg', + }, + { + icon: Dumbbell, + title: 'Fitness & Activities', + description: 'State-of-the-art gym, yoga classes, water sports, and guided excursions.', + image: '/images/cdn/photo-1540497077202-7c8a3999166f.jpg', + }, + { + icon: Wine, + title: 'Premium Bar & Lounge', + description: 'Top-shelf spirits, craft cocktails, and an extensive wine list, all day long.', + image: '/images/cdn/photo-1514362545857-3bc16c4c7d1b.jpg', + }, + { + icon: Sun, + title: 'Private Beach', + description: 'White sand, crystal-clear waters, and beach service with complimentary towels.', + image: '/images/cdn/photo-1507525428034-b723cf961d3e.jpg', + }, +] + +const GALLERY_IMAGES = [ + { src: '/images/cdn/photo-1566073771259-6a8506099945.jpg', caption: 'Oceanfront Suite' }, + { src: '/images/cdn/photo-1520250497591-112f2f40a3f4.jpg', caption: 'Resort Pool' }, + { src: '/images/cdn/photo-1551882547-ff40c63fe5fa.jpg', caption: 'Lobby & Gardens' }, + { src: '/images/cdn/photo-1571896349842-33c89424de2d.jpg', caption: 'Beachfront Dining' }, + { src: '/images/cdn/photo-1584132967334-10e028bd69f7.jpg', caption: 'Sunset Terrace' }, + { src: '/images/cdn/photo-1615460549969-36fa19521a4f.jpg', caption: 'Spa Retreat' }, + { src: '/images/cdn/photo-1602002418816-5c0aeef426aa.jpg', caption: 'Luxury Room' }, + { src: '/images/cdn/photo-1596436889106-be35e843f974.jpg', caption: 'Beach Paradise' }, +] + +function HorizontalGallery() { + const scrollRef = useRef(null) + const [canScrollLeft, setCanScrollLeft] = useState(false) + const [canScrollRight, setCanScrollRight] = useState(true) + + const checkScroll = () => { + if (!scrollRef.current) return + const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current + setCanScrollLeft(scrollLeft > 10) + setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 10) + } + + useEffect(() => { + const el = scrollRef.current + if (!el) return + el.addEventListener('scroll', checkScroll, { passive: true }) + checkScroll() + return () => el.removeEventListener('scroll', checkScroll) + }, []) + + const scroll = (direction: 'left' | 'right') => { + if (!scrollRef.current) return + const amount = scrollRef.current.clientWidth * 0.7 + scrollRef.current.scrollBy({ left: direction === 'left' ? -amount : amount, behavior: 'smooth' }) + } + + return ( +
+
+ {GALLERY_IMAGES.map((img, i) => ( +
+
+ {img.caption} +
+

+ {img.caption} +

+
+
+ ))} +
+ + {canScrollLeft && ( + + )} + {canScrollRight && ( + + )} +
+ ) +} + +export default function LP07ResortPreview() { + return ( +
+ + + {/* Full-Screen Hero with Video Placeholder */} +
+ Luxury resort aerial view +
+ + {/* Play Button Overlay */} +
+ +

+ Your Future Awaits +

+

+ Tour Your Future
+ Resort +

+

+ Step inside the all-inclusive luxury resorts where your next vacation begins. + 5 days. 4 nights. From just ${PAYMENT_CONFIG.monthlyPrice}/month. +

+ + Reserve Your Stay + + +
+ + {/* Scroll indicator */} +
+

Scroll to explore

+
+
+
+
+
+ + {/* Introduction */} +
+
+

+ An Immersive Experience +

+

+ Where Elegance Meets Paradise +

+

+ Our hand-selected partner resorts represent the finest in Mexican hospitality. + Each property offers world-class amenities, stunning natural beauty, and the kind + of service that turns a vacation into a lifelong memory. +

+
+
+ + {/* Resort Amenity Showcase */} +
+
+
+

+ Resort Amenities +

+

+ Everything You Could Dream Of +

+
+ +
+ {AMENITIES.map((amenity) => ( +
+
+ {amenity.title} +
+
+ +
+
+
+

+ {amenity.title} +

+

{amenity.description}

+
+
+ ))} +
+
+
+ + {/* Horizontal Scroll Gallery */} +
+
+
+

+ Visual Tour +

+

+ A Glimpse of Paradise +

+
+ +
+
+ + {/* Destination Carousel */} +
+
+
+

+ Choose Your Destination +

+

+ Four Stunning Locations +

+
+ +
+
+ + {/* Testimonials */} +
+
+
+

+ Guest Experiences +

+

+ Words From Our Guests +

+
+
+ {TESTIMONIALS.slice(0, 4).map((t, i) => ( + + ))} +
+
+
+ + {/* Pricing Section */} +
+
+

+ Your Investment +

+

+ An Extraordinary Value +

+

+ Five days of all-inclusive luxury for less than a single night at most resorts. +

+
+ +
+
+
+ + {/* Signup Form */} +
+
+ Beach sunset +
+
+
+
+

+ Reserve Now +

+

+ Begin Your Journey +

+

+ Secure your all-inclusive vacation certificate today +

+
+
+ +
+
+ +
+
+
+ + {/* FAQ */} +
+
+
+

+ Questions +

+ + +

+ Frequently Asked +

+
+
+ +
+
+
+ + {/* Footer */} +
+

+ Mexico Paradise Vacations +

+

+ Luxury experiences, extraordinary value. +

+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP08SplitDecision.tsx b/src/components/lp/pages/LP08SplitDecision.tsx new file mode 100644 index 0000000..742ec8c --- /dev/null +++ b/src/components/lp/pages/LP08SplitDecision.tsx @@ -0,0 +1,455 @@ +'use client' + +import { useState } from 'react' +import { + Check, + ArrowRight, + TreePalm, + Waves, + Sun, + UtensilsCrossed, + Wine, + Shield, + Star, + MapPin, +} from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import PricingDisplay from '@/components/lp/shared/PricingDisplay' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, DESTINATIONS } from '@/app/lp/_config/types' + +const AMBER = '#FF6F00' +const BLUE = '#1565C0' +const PINK = '#E91E63' + +const cancun = DESTINATIONS.find(d => d.name === 'Cancun')! +const cabo = DESTINATIONS.find(d => d.name === 'Cabo San Lucas')! + +const SHARED_BENEFITS = [ + { icon: Waves, text: '5 days / 4 nights all-inclusive' }, + { icon: UtensilsCrossed, text: 'Unlimited meals & drinks included' }, + { icon: Sun, text: 'Beach & pool access all day' }, + { icon: Wine, text: 'Premium bar & cocktail service' }, + { icon: Shield, text: '30-day money-back guarantee' }, + { icon: Star, text: 'Rated 4.8/5 by travelers' }, +] + +const CANCUN_HIGHLIGHTS = [ + 'World-famous turquoise waters', + 'Vibrant nightlife & entertainment', + 'Mayan ruins nearby (Chichen Itza)', + 'Snorkeling in the Great Mesoamerican Reef', + 'Year-round tropical weather', +] + +const CABO_HIGHLIGHTS = [ + 'Dramatic cliffs meet the Pacific Ocean', + 'Iconic El Arco rock formation', + 'World-class sport fishing', + 'Desert-meets-ocean landscape', + 'Luxurious boutique resort vibes', +] + +function DestinationCard({ + name, + tagline, + image, + highlights, + color, + side, +}: { + name: string + tagline: string + image: string + highlights: string[] + color: string + side: 'left' | 'right' +}) { + return ( +
+
+ {name} +
+
+ +

+ {name} +

+

{tagline}

+
+
+
+
    + {highlights.map((h, i) => ( +
  • + + + {h} + +
  • + ))} +
+
+
+ ) +} + +export default function LP08SplitDecision() { + const [hoveredSide, setHoveredSide] = useState<'left' | 'right' | null>(null) + + return ( +
+ + + {/* Split-Screen Hero */} +
+
+ {/* Cancun Side */} +
setHoveredSide('left')} + onMouseLeave={() => setHoveredSide(null)} + style={{ + flex: hoveredSide === 'left' ? 1.2 : hoveredSide === 'right' ? 0.8 : 1, + }} + > + Cancun +
+
+

+ Option A +

+

+ Cancun +

+

{cancun.tagline}

+
+
+ + {/* Cabo Side */} +
setHoveredSide('right')} + onMouseLeave={() => setHoveredSide(null)} + style={{ + flex: hoveredSide === 'right' ? 1.2 : hoveredSide === 'left' ? 0.8 : 1, + }} + > + Cabo San Lucas +
+
+

+ Option B +

+

+ Cabo +

+

{cabo.tagline}

+
+
+
+ + {/* Center divider text */} +
+
+ + VS + +
+
+ + {/* Bottom text overlay */} +
+

+ The Only Question Is WHICH Paradise +

+
+
+ + {/* "Both Include" Section */} +
+
+

+ No Matter Which You Choose... +

+

+ Both destinations include everything below. +

+
+ {SHARED_BENEFITS.map((benefit) => ( +
+
+ +
+

+ {benefit.text} +

+
+ ))} +
+
+
+ + {/* Detailed Split Comparison */} +
+
+

+ Compare Your Options +

+
+
+ +
+
+ +
+
+
+

+ Plus Riviera Maya and Puerto Vallarta are also available! +

+

+ Your certificate works at any of our 4 stunning destinations. +

+
+
+
+ + {/* The Choice is Easy */} +
+
+

+ Forget "Cancun vs. Cabo"— +
The Real Choice Is Vacation vs. No Vacation +

+

+ For just ${PAYMENT_CONFIG.monthlyPrice}/month, you get 5 days of all-inclusive paradise. + Pick your destination later. Lock in the price now. +

+ + Choose Your Paradise + + +
+
+ + {/* Other Destinations Preview */} +
+
+

+ Also Available With Your Certificate +

+
+ {DESTINATIONS.filter(d => d.name !== 'Cancun' && d.name !== 'Cabo San Lucas').map((dest) => ( +
+ {dest.name} +
+
+

+ {dest.name} +

+

{dest.tagline}

+
+
+ ))} +
+
+
+ + {/* Pricing */} +
+
+

+ One Price. Any Destination. +

+

+ Your certificate is valid at all 4 locations. +

+
+ +
+
+
+ + {/* Testimonials */} +
+
+

+ What Travelers Are Saying +

+
+ {TESTIMONIALS.slice(0, 3).map((t, i) => ( + + ))} +
+
+
+ + {/* Signup Form */} +
+
+
+
+
+
+

+ Ready To Decide? +

+

+ Secure your certificate now. Choose your destination later. +

+
+
+ +
+
+ +
+
+
+ + {/* FAQ */} + + +
+
+

+ Common Questions +

+ +
+
+ + {/* Footer */} +
+

+ Mexico Paradise Vacations. All rights reserved. +

+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP09Calculator.tsx b/src/components/lp/pages/LP09Calculator.tsx new file mode 100644 index 0000000..afed3d9 --- /dev/null +++ b/src/components/lp/pages/LP09Calculator.tsx @@ -0,0 +1,451 @@ +'use client' + +import { useState, useMemo } from 'react' +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from 'recharts' +import { + Calculator, + TrendingDown, + DollarSign, + Check, + ArrowRight, + BarChart3, + Percent, + PiggyBank, + Shield, +} from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import ComparisonTable from '@/components/lp/shared/ComparisonTable' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS } from '@/app/lp/_config/types' + +const PRIMARY_BLUE = '#0D47A1' +const GREEN = '#00C853' +const BG = '#E8EAF6' + +const BOOKING_OPTIONS = [ + { label: 'Budget Hotel', nights: 5, costPerNight: 180, allInclusive: false, mealsPerDay: 80, drinksPerDay: 30 }, + { label: 'Mid-Range Resort', nights: 5, costPerNight: 350, allInclusive: false, mealsPerDay: 120, drinksPerDay: 50 }, + { label: 'All-Inclusive Resort', nights: 5, costPerNight: 550, allInclusive: true, mealsPerDay: 0, drinksPerDay: 0 }, + { label: 'Luxury Resort', nights: 5, costPerNight: 800, allInclusive: true, mealsPerDay: 0, drinksPerDay: 0 }, +] + +function InteractiveCalculator() { + const [selectedOption, setSelectedOption] = useState(2) + const [travelers, setTravelers] = useState(2) + + const option = BOOKING_OPTIONS[selectedOption] + + const traditionalCost = useMemo(() => { + const roomCost = option.costPerNight * option.nights + const mealCost = option.allInclusive ? 0 : option.mealsPerDay * option.nights * travelers + const drinkCost = option.allInclusive ? 0 : option.drinksPerDay * option.nights * travelers + return roomCost + mealCost + drinkCost + }, [selectedOption, travelers, option]) + + const ourCost = PAYMENT_CONFIG.oneTimePrice + const savings = Math.max(0, traditionalCost - ourCost) + const savingsPercent = traditionalCost > 0 ? Math.round((savings / traditionalCost) * 100) : 0 + + const chartData = [ + { + name: option.label, + cost: traditionalCost, + fill: '#EF5350', + }, + { + name: 'Expedia Avg', + cost: Math.round(traditionalCost * 0.85), + fill: '#FF9800', + }, + { + name: 'Mexico Paradise', + cost: ourCost, + fill: GREEN, + }, + ] + + return ( +
+ {/* Controls */} +
+
+ +
+ {BOOKING_OPTIONS.map((opt, i) => ( + + ))} +
+
+
+ +
+ {[1, 2, 3, 4].map((n) => ( + + ))} +
+

+ Certificate covers 2 guests. Additional guests at discounted rate. +

+ + {/* Savings Highlight */} +
+

+ YOUR ESTIMATED SAVINGS +

+

+ ${savings.toLocaleString()} +

+

+ That's {savingsPercent}% less than {option.label} +

+
+
+
+ + {/* Chart */} +
+

+ Cost Comparison: 5-Day Mexico Vacation +

+
+ + + + + `$${v}`} + /> + [`$${value.toLocaleString()}`, 'Total Cost']} + contentStyle={{ fontFamily: 'var(--font-ibm-plex)', fontSize: '13px' }} + /> + + {chartData.map((entry, index) => ( + + ))} + + + +
+
+ Traditional + Online Travel + Mexico Paradise +
+
+
+ ) +} + +function StatCard({ + icon: Icon, + value, + label, + color = PRIMARY_BLUE, +}: { + icon: React.ElementType + value: string + label: string + color?: string +}) { + return ( +
+ +

+ {value} +

+

+ {label} +

+
+ ) +} + +export default function LP09Calculator() { + return ( +
+ + + {/* Header */} +
+
+
+ + + Mexico Paradise + +
+ + Get Started + +
+
+ + {/* Hero */} +
+
+
+ + Data-Driven Travel Savings +
+

+ The Math
+ Doesn't Lie +

+

+ See exactly how much you save with a Mexico Paradise Vacation Certificate + compared to booking through traditional channels. +

+ + {/* Stats Row */} +
+ + + + +
+
+
+ + {/* Interactive Calculator */} +
+
+
+

+ Interactive Cost Calculator +

+

+ Compare our price against different booking options. Adjust the inputs below. +

+
+ +
+
+ + {/* Comparison Table */} +
+
+
+

+ Feature-by-Feature Breakdown +

+

+ It's not just about price. See what's included. +

+
+
+ +
+
+
+ + {/* Value Breakdown */} +
+
+

+ What's Included In Your ${PAYMENT_CONFIG.oneTimePrice} +

+

+ Every item below is included. No hidden fees. No surprises. +

+
+ {[ + { item: '5 Days / 4 Nights', value: '$1,200 value' }, + { item: 'All Meals Included', value: '$800 value' }, + { item: 'Unlimited Drinks', value: '$400 value' }, + { item: 'Resort Amenities', value: '$400 value' }, + { item: 'Bring A Guest Free', value: '$500 value' }, + { item: 'Flexible Booking', value: '$200 value' }, + ].map((item) => ( +
+ +
+

{item.item}

+

{item.value}

+
+
+ ))} +
+
+

+ Total retail value:{' '} + $3,500{' '} + + ${PAYMENT_CONFIG.oneTimePrice} + +

+
+
+
+ + {/* Social Proof */} +
+
+

+ Verified Reviews +

+

+ From travelers who ran the numbers and took the trip. +

+
+ {TESTIMONIALS.slice(0, 3).map((t, i) => ( + + ))} +
+
+
+ + {/* Pricing + Form */} +
+
+
+

+ Ready To Save? +

+

+ Lock in your vacation certificate at today's price. +

+
+
+
+
+

$3,000+

+

+ ${PAYMENT_CONFIG.monthlyPrice}/mo +

+

+ or ${PAYMENT_CONFIG.oneTimePrice} one-time +

+
+
+ +
+
+ +
+
+
+ + + + {/* FAQ */} +
+
+

+ Frequently Asked Questions +

+
+ +
+
+
+ + {/* Bottom CTA */} +
+
+

+ The numbers speak for themselves. +

+

+ Save an average of $2,601 on your next Mexico vacation. +

+ + Start Saving Now + + +
+
+ + {/* Footer */} +
+

+ Mexico Paradise Vacations. All rights reserved. +

+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP10Countdown.tsx b/src/components/lp/pages/LP10Countdown.tsx new file mode 100644 index 0000000..a37e8df --- /dev/null +++ b/src/components/lp/pages/LP10Countdown.tsx @@ -0,0 +1,501 @@ +'use client' + +import { useState, useEffect } from 'react' +import { + Flame, + Check, + ArrowRight, + AlertTriangle, + Zap, + Star, + Shield, + MapPin, + Clock, + Gift, + Lock, +} from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, DESTINATIONS } from '@/app/lp/_config/types' + +const BLACK = '#000000' +const RED = '#F44336' +const AMBER = '#FFC107' + +const FEATURES = [ + { + icon: MapPin, + title: '4 Premium Destinations', + description: 'Cancun, Cabo San Lucas, Riviera Maya, or Puerto Vallarta', + }, + { + icon: Clock, + title: '5 Days / 4 Nights', + description: 'A full vacation, not a weekend getaway', + }, + { + icon: Gift, + title: 'All-Inclusive Package', + description: 'Every meal, every drink, every activity included', + }, + { + icon: Star, + title: '4-5 Star Resorts', + description: 'Luxury properties with world-class amenities', + }, + { + icon: Shield, + title: '30-Day Money Back', + description: 'Full refund if you change your mind', + }, + { + icon: Zap, + title: 'Flexible Booking', + description: '18 months to choose your travel dates', + }, +] + +function PulsingDot() { + return ( + + + + + ) +} + +function SpotsCounter() { + const [spots, setSpots] = useState(47) + + useEffect(() => { + const interval = setInterval(() => { + setSpots(prev => { + if (prev <= 12) return prev + return Math.random() > 0.7 ? prev - 1 : prev + }) + }, 30000) + return () => clearInterval(interval) + }, []) + + return ( +
+ + {spots} + spots remaining +
+ ) +} + +export default function LP10Countdown() { + return ( +
+ + + {/* Urgency Bar */} +
+
+ + + LIMITED TIME OFFER — PRICE INCREASES WHEN TIMER HITS ZERO + + +
+
+ + {/* Countdown Hero */} +
+ {/* Background */} +
+ Dark beach scene +
+
+ + {/* Animated background particles effect using CSS */} +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+ +
+
+ + + Exclusive Limited Drop + + +
+ +

+ OFFER EXPIRES
+ WHEN THE CLOCK
+ HITS ZERO +

+ + {/* Giant Countdown */} +
+ +
+ +
+ +
+ + + Claim Your Certificate Now + + +

+ ${PAYMENT_CONFIG.monthlyPrice}/mo for {PAYMENT_CONFIG.totalMonths} months or ${PAYMENT_CONFIG.oneTimePrice} one-time +

+
+ + {/* Scroll indicator */} +
+
+
+
+
+
+ + {/* "What You're Getting" Section */} +
+
+
+

+ WHAT YOU'RE GETTING +

+

+ 5 days of all-inclusive luxury. Here's the full breakdown. +

+
+ +
+ {FEATURES.map((feature) => ( +
+ +

+ {feature.title} +

+

+ {feature.description} +

+
+ ))} +
+
+
+ + {/* Price Anchor */} +
+
+

+ THE REAL COST OF NOT ACTING +

+
+
+

Book Direct

+

+ $3,000+ +

+
+
+

Expedia / Hotels.com

+

+ $2,500+ +

+
+
+

Mexico Paradise

+

+ ${PAYMENT_CONFIG.oneTimePrice} +

+
+
+

+ That's ${PAYMENT_CONFIG.monthlyPrice}/month — + less than a streaming subscription for a luxury vacation. +

+
+
+ + {/* Destination Previews */} +
+
+
+

+ CHOOSE YOUR DESTINATION +

+

+ All four locations included with your certificate. +

+
+ +
+ {DESTINATIONS.map((dest) => ( +
+ {dest.name} +
+
+

+ {dest.name} +

+

{dest.tagline}

+
+
+ +
+
+ ))} +
+
+
+ + {/* Testimonials */} +
+
+

+ WHAT OTHERS ARE SAYING +

+
+ {TESTIMONIALS.slice(0, 4).map((t, i) => ( + + ))} +
+
+
+ + {/* Mini Countdown Reminder */} +
+
+

+ TIME IS RUNNING OUT +

+
+ +
+ + Don't Miss Out + + +
+
+ + {/* Trust Badges */} +
+
+

+ SECURE & GUARANTEED +

+
+ {[ + { icon: Lock, label: '256-bit SSL' }, + { icon: Shield, label: '30-Day Guarantee' }, + { icon: Star, label: '4.8/5 Rating' }, + { icon: Check, label: 'Verified Business' }, + ].map((badge) => ( +
+
+ +
+ {badge.label} +
+ ))} +
+
+
+ + {/* Signup Form */} +
+
+
+ +
+
+
+ + + Secure Your Spot + + +
+

+ CLAIM YOUR CERTIFICATE +

+

+ Before the clock runs out and this price disappears. +

+
+ +
+ {/* Mini countdown in form */} +
+ + + remaining +
+ + +
+ +
+ +
+
+
+ + {/* FAQ */} + + +
+
+

+ QUESTIONS? ANSWERED. +

+ +
+
+ + {/* Final CTA */} +
+
+

+ THE CLOCK IS TICKING +

+
+ +
+ + Last Chance — Get It Now + + +
+
+ + {/* Footer */} +
+

+ Mexico Paradise Vacations. All rights reserved. +

+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP11TheGuide.tsx b/src/components/lp/pages/LP11TheGuide.tsx new file mode 100644 index 0000000..b1a3926 --- /dev/null +++ b/src/components/lp/pages/LP11TheGuide.tsx @@ -0,0 +1,429 @@ +'use client' + +import { useState, useEffect } from 'react' +import { BookOpen, Clock, User, ChevronRight, MapPin } from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { TESTIMONIALS, DESTINATIONS, PAYMENT_CONFIG } from '@/app/lp/_config/types' + +export default function LP11TheGuide() { + const [scrollProgress, setScrollProgress] = useState(0) + + useEffect(() => { + const handleScroll = () => { + const scrollTop = window.scrollY + const docHeight = document.documentElement.scrollHeight - window.innerHeight + const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0 + setScrollProgress(Math.min(progress, 100)) + } + window.addEventListener('scroll', handleScroll, { passive: true }) + return () => window.removeEventListener('scroll', handleScroll) + }, []) + + return ( +
+ + + {/* Reading Progress Bar */} +
+
+
+ + {/* Top Navigation Bar */} + + + {/* Article Header */} +
+
+ + Travel Guide + + + + 12 min read + + + + Editorial Team + +
+ +

+ 5 Secrets to Luxury Mexico Vacations on a Budget +

+ +

+ How savvy travelers are staying at 5-star all-inclusive resorts in Cancun, Cabo, + and the Riviera Maya for less than $1.30 a day. Yes, you read that right. +

+ +
+ Author +
+

Marco Rivera

+

Travel Editor -- Updated March 2026

+
+
+
+ + {/* Hero Image */} +
+
+ Cancun beach resort at sunset +
+

Cancun, Mexico -- One of four destinations in the program

+
+
+
+ + {/* Article Body */} +
+ {/* Intro paragraph */} +

+ Every year, millions of Americans dream of a tropical getaway but convince themselves + it's out of reach. The average all-inclusive Mexico vacation costs between $2,500 and + $4,000 per couple. But what if there was a way to enjoy the same crystal-clear waters, + gourmet dining, and luxury suites for a fraction of that price? +

+ +

+ After spending three years investigating vacation certificate programs across the + industry, our editorial team uncovered a legitimate pathway that thousands of travelers + are already using. Here's what we found. +

+ + {/* Secret #1 */} +

+ Secret #1: The Resort Presentation Model +

+ +

+ Here's what most people don't realize: luxury resorts in Mexico have a massive + customer acquisition problem. Their rooms are often half-empty during shoulder seasons, + and they're willing to offer deeply discounted stays to potential future members. +

+ +

+ The catch? You attend a 90-minute resort presentation during your stay. No obligation + to buy anything. You listen, you say "no thank you" if it's not for you, + and you go back to sipping margaritas by the infinity pool. +

+ +

+ This model has existed for decades in the timeshare industry, but a new wave of + vacation certificate programs has made it accessible, transparent, and genuinely + affordable. The result? A 5-night, all-inclusive stay at a resort that normally + charges $400+/night, available for as little as $39/month. +

+ + {/* Inline destination images */} +
+ Cabo San Lucas + Riviera Maya +
+ + {/* Secret #2 */} +

+ Secret #2: Timing Is Everything +

+ +

+ The second secret experienced budget-luxury travelers know is that when you + travel matters just as much as how you book. Mexico's shoulder seasons -- + May through mid-June and September through November -- offer the same stunning weather + with a fraction of the crowds. +

+ +

+ Certificate programs give you an 18-month window to book, which means you can + strategically choose dates when resorts roll out their best perks: room upgrades, + spa credits, and premium dining packages. One couple we interviewed scored a + suite upgrade that would have cost $200/night extra -- simply by traveling in + early October. +

+ + {/* Secret #3 preview */} +

+ Secret #3: The All-Inclusive Advantage +

+ +

+ Most travelers underestimate how much they spend on food, drinks, and activities + during a vacation. Our research found the average couple spends $150-$250 per day on + dining and entertainment alone. With an all-inclusive certificate, every meal, every + cocktail, every poolside snack is already covered. +

+ + {/* Callout box */} +
+

+ By the numbers +

+

+ ${PAYMENT_CONFIG.monthlyPrice}/month x {PAYMENT_CONFIG.totalMonths} months = ${PAYMENT_CONFIG.totalPrice} total +

+

+ vs. the average all-inclusive vacation cost of $3,200+ per couple. + That's over $2,800 in savings. +

+
+ +

+ But there are two more secrets that can save you even more -- and they're the + ones most "travel hack" articles won't tell you about... +

+ + {/* Content Gate - Fade out effect */} +
+

+ Secret #4 involves a little-known booking strategy that experienced certificate + holders use to maximize their stay. And Secret #5? It's the one thing that + separates travelers who have a "good" vacation from those who have the + trip of a lifetime... +

+
+
+
+ + {/* Ebook Capture Section */} +
+
+ +

+ Read the Full Guide -- Free +

+

+ Get all 5 secrets plus our destination comparison chart, packing checklist, + and insider resort ratings. Delivered instantly to your inbox. +

+
+ +
+

+ Join 12,400+ readers who downloaded this guide. +

+
+
+ + {/* Destination Preview */} +
+

+ Four Destinations, One Certificate +

+
+ {DESTINATIONS.map((dest) => ( +
+ {dest.name} +
+
+

+ {dest.name} +

+

{dest.tagline}

+
+
+ ))} +
+
+ + {/* Testimonials */} +
+

+ What Travelers Are Saying +

+
+ {TESTIMONIALS.slice(0, 4).map((t, i) => ( + + ))} +
+
+ + {/* Secondary PayNow Section */} +
+
+

+ Ready to Skip the Guide? +

+

+ Claim your 5-day, 4-night all-inclusive Mexico vacation certificate now. + Just ${PAYMENT_CONFIG.monthlyPrice}/month or ${PAYMENT_CONFIG.oneTimePrice} one-time. +

+
+ +
+ +
+
+ + + + {/* FAQ */} +
+

+ Frequently Asked Questions +

+ +
+ + {/* Footer */} + + + +
+ ) +} diff --git a/src/components/lp/pages/LP12Dreamboard.tsx b/src/components/lp/pages/LP12Dreamboard.tsx new file mode 100644 index 0000000..a2428e8 --- /dev/null +++ b/src/components/lp/pages/LP12Dreamboard.tsx @@ -0,0 +1,389 @@ +'use client' + +import { useState } from 'react' +import { Heart, Sparkles, Plane, Star } from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import PricingDisplay from '@/components/lp/shared/PricingDisplay' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { TESTIMONIALS, PAYMENT_CONFIG } from '@/app/lp/_config/types' + +const DREAM_PHOTOS = [ + { + src: '/images/cdn/photo-1552074284-5e88ef1aef18.jpg', + label: 'Cancun Sunsets', + height: 'h-72', + }, + { + src: '/images/cdn/photo-1507525428034-b723cf961d3e.jpg', + label: 'Pristine Beaches', + height: 'h-48', + }, + { + src: '/images/cdn/photo-1551882547-ff40c63fe5fa.jpg', + label: 'Luxury Pools', + height: 'h-64', + }, + { + src: '/images/cdn/photo-1581710862235-eb6e05d8783f.jpg', + label: 'Riviera Maya', + height: 'h-56', + }, + { + src: '/images/cdn/photo-1544551763-46a013bb70d5.jpg', + label: 'Crystal Waters', + height: 'h-80', + }, + { + src: '/images/cdn/photo-1580846629083-02669741360a.jpg', + label: 'Cabo Magic', + height: 'h-48', + }, + { + src: '/images/cdn/photo-1581710862235-eb6e05d8783f.jpg', + label: 'Ocean Views', + height: 'h-60', + }, + { + src: '/images/cdn/photo-1585793753011-397e6e4668d6.jpg', + label: 'Golden Horizons', + height: 'h-72', + }, + { + src: '/images/cdn/photo-1519046904884-53103b34b206.jpg', + label: 'Beach Bliss', + height: 'h-52', + }, + { + src: '/images/cdn/photo-1571896349842-33c89424de2d.jpg', + label: 'Resort Paradise', + height: 'h-72', + }, + { + src: '/images/cdn/photo-1473116763249-2faaef81ccda.jpg', + label: 'Puerto Vallarta', + height: 'h-56', + }, + { + src: '/images/cdn/photo-1540541338287-41700207dee6.jpg', + label: 'Coastal Dreams', + height: 'h-64', + }, +] + +export default function LP12Dreamboard() { + const [likedPhotos, setLikedPhotos] = useState>(new Set()) + + const toggleLike = (index: number) => { + setLikedPhotos((prev) => { + const next = new Set(prev) + if (next.has(index)) { + next.delete(index) + } else { + next.add(index) + } + return next + }) + } + + return ( +
+ + + {/* Hero Section */} +
+ {/* Decorative circles */} +
+
+
+ +
+
+ + + Build Your Dream Vacation Board + +
+ +

+ Where Will Your +
+ + Dreams Take You? + +

+ +

+ Close your eyes. Picture yourself on a pristine Mexican beach, cocktail in hand, + waves lapping at your feet. Now open them -- and start planning. +

+ +
+ + + + Downloaded by 12,400+ dreamers + +
+
+
+ + {/* Masonry Photo Grid */} +
+

+ Your Vacation Mood Board +

+

+ Tap the heart on your favorites -- your dream vacation is closer than you think +

+ +
+ {DREAM_PHOTOS.map((photo, i) => ( +
+ {photo.label} +
+ + {/* Like button */} + + + {/* Label */} +
+

{photo.label}

+
+
+ ))} +
+ + {likedPhotos.size > 0 && ( +
+

+ + You've saved {likedPhotos.size} dream{likedPhotos.size !== 1 ? 's' : ''} -- get the guide to make them real! +

+
+ )} +
+ + {/* Ebook Capture Overlay Section */} +
+
+ {/* Decorative background */} +
+
+
+
+ +
+

+ Turn Dreams into Plans +

+

+ Get our free "Budget Luxury Travel" guide with 5 secrets to + luxury Mexico vacations on a budget. +

+
+ + +
+
+
+ + {/* Destinations showcase */} +
+

+ Four Dreamy Destinations +

+ +
+ {[ + { name: 'Cancun', img: '/images/cdn/photo-1552074284-5e88ef1aef18.jpg' }, + { name: 'Cabo San Lucas', img: '/images/cdn/photo-1580846629083-02669741360a.jpg' }, + { name: 'Riviera Maya', img: '/images/cdn/photo-1581710862235-eb6e05d8783f.jpg' }, + { name: 'Puerto Vallarta', img: '/images/cdn/photo-1473116763249-2faaef81ccda.jpg' }, + ].map((d) => ( +
+ {d.name} +
+
+

+ {d.name} +

+
+
+ ))} +
+
+ + {/* Testimonials */} +
+

+ Dreamers Who Made It Real +

+
+ {TESTIMONIALS.slice(0, 3).map((t, i) => ( + + ))} +
+
+ + {/* PayNow Section */} +
+
+ +

+ Your Dream Vacation Starts Here +

+

+ 5 days, 4 nights all-inclusive in Mexico. + Starting at just ${PAYMENT_CONFIG.monthlyPrice}/month. +

+ + + + + + +
+
+ + + + {/* Footer */} + + + +
+ ) +} diff --git a/src/components/lp/pages/LP13QuizFunnel.tsx b/src/components/lp/pages/LP13QuizFunnel.tsx new file mode 100644 index 0000000..1b3f362 --- /dev/null +++ b/src/components/lp/pages/LP13QuizFunnel.tsx @@ -0,0 +1,508 @@ +'use client' + +import { useState } from 'react' +import { ChevronRight, ChevronLeft, Compass, Sun, Moon, Users, Wallet, MapPin, Sparkles, Check } from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { TESTIMONIALS, PAYMENT_CONFIG } from '@/app/lp/_config/types' + +interface QuizOption { + label: string + value: string + icon: React.ReactNode + description: string +} + +interface QuizQuestion { + id: number + question: string + subtitle: string + options: QuizOption[] +} + +const QUESTIONS: QuizQuestion[] = [ + { + id: 1, + question: 'What calls to you more?', + subtitle: 'Choose the vibe that matches your dream vacation', + options: [ + { + label: 'Beach & Relaxation', + value: 'beach', + icon: , + description: 'Soft sand, turquoise water, total bliss', + }, + { + label: 'Adventure & Exploration', + value: 'adventure', + icon: , + description: 'Ruins, cenotes, snorkeling, zip-lines', + }, + ], + }, + { + id: 2, + question: 'How do you spend your evenings?', + subtitle: 'Your ideal vacation night looks like...', + options: [ + { + label: 'Vibrant Nightlife', + value: 'nightlife', + icon: , + description: 'Clubs, bars, live music, dancing', + }, + { + label: 'Peaceful Relaxation', + value: 'relaxation', + icon: , + description: 'Spa, sunset cocktails, stargazing', + }, + ], + }, + { + id: 3, + question: "What's your budget style?", + subtitle: 'How you like to plan your spending', + options: [ + { + label: 'Spread It Out', + value: 'monthly', + icon: , + description: `$${PAYMENT_CONFIG.monthlyPrice}/month -- easy payments`, + }, + { + label: 'Pay Once & Done', + value: 'one-time', + icon: , + description: `$${PAYMENT_CONFIG.oneTimePrice} one-time -- save more`, + }, + ], + }, + { + id: 4, + question: "Who's coming with you?", + subtitle: 'Your travel crew matters for the perfect destination', + options: [ + { + label: 'Partner / Couple', + value: 'partner', + icon: , + description: 'Romantic getaway for two', + }, + { + label: 'Friends / Group', + value: 'group', + icon: , + description: 'Fun times with the crew', + }, + ], + }, +] + +interface DestinationResult { + name: string + tagline: string + description: string + image: string + highlights: string[] +} + +const RESULTS: Record = { + cancun: { + name: 'Cancun', + tagline: 'Your Perfect Match!', + description: + 'With its stunning beaches, vibrant nightlife, and easy accessibility, Cancun is the ideal destination for your travel style. Enjoy world-class resorts, crystal-clear Caribbean waters, and endless entertainment options.', + image: '/images/cdn/photo-1510097467424-192d713fd8b2.jpg', + highlights: ['Pristine white sand beaches', 'World-class nightlife scene', 'Nearby Mayan ruins', 'Water sports paradise'], + }, + cabo: { + name: 'Cabo San Lucas', + tagline: 'Your Dream Destination Awaits!', + description: + 'Dramatic desert landscapes meet the sea in Cabo San Lucas. Perfect for couples seeking romance and adventure alike, with whale watching, sunset cruises, and luxury dining.', + image: '/images/cdn/photo-1580415200778-625cb1890ab5.jpg', + highlights: ['Iconic El Arco landmark', 'Luxury resort experiences', 'Whale watching season', 'Desert-meets-ocean scenery'], + }, + riviera: { + name: 'Riviera Maya', + tagline: 'Adventure Meets Paradise!', + description: + 'The Riviera Maya offers the best of both worlds -- ancient Mayan ruins, mysterious cenotes, and pristine Caribbean coastline. An explorer\'s dream with all the luxury you deserve.', + image: '/images/cdn/photo-1518638150340-f706e86654de.jpg', + highlights: ['Sacred cenote swimming', 'Tulum ruins by the sea', 'Eco-adventure parks', 'Secluded beach coves'], + }, + vallarta: { + name: 'Puerto Vallarta', + tagline: 'Culture & Coast Combined!', + description: + 'Puerto Vallarta charms with its cobblestone streets, vibrant art scene, and breathtaking Pacific sunsets. The warmth of Mexican culture shines brightest here.', + image: '/images/cdn/photo-1585793753011-397e6e4668d6.jpg', + highlights: ['Stunning Pacific sunsets', 'Rich cultural heritage', 'Malecon boardwalk', 'Authentic Mexican cuisine'], + }, +} + +function getResult(answers: Record): DestinationResult { + const a1 = answers[1] + const a2 = answers[2] + const a4 = answers[4] + + if (a1 === 'adventure' && a2 === 'relaxation') return RESULTS.riviera + if (a1 === 'beach' && a2 === 'nightlife') return RESULTS.cancun + if (a1 === 'beach' && a4 === 'partner') return RESULTS.cabo + if (a1 === 'adventure' && a2 === 'nightlife') return RESULTS.cancun + if (a4 === 'partner' && a2 === 'relaxation') return RESULTS.cabo + if (a1 === 'adventure') return RESULTS.riviera + if (a4 === 'group') return RESULTS.cancun + return RESULTS.vallarta +} + +export default function LP13QuizFunnel() { + const [step, setStep] = useState<'intro' | 'quiz' | 'result'>('intro') + const [currentQuestion, setCurrentQuestion] = useState(0) + const [answers, setAnswers] = useState>({}) + + const handleAnswer = (questionId: number, value: string) => { + const newAnswers = { ...answers, [questionId]: value } + setAnswers(newAnswers) + + // Auto-advance after short delay + setTimeout(() => { + if (currentQuestion < QUESTIONS.length - 1) { + setCurrentQuestion((prev) => prev + 1) + } else { + setStep('result') + } + }, 400) + } + + const goBack = () => { + if (currentQuestion > 0) { + setCurrentQuestion((prev) => prev - 1) + } else { + setStep('intro') + } + } + + const result = step === 'result' ? getResult(answers) : null + const progress = step === 'quiz' ? ((currentQuestion + 1) / QUESTIONS.length) * 100 : 0 + + return ( +
+ + + {/* Intro Screen */} + {step === 'intro' && ( +
+ {/* Background decoration */} +
+
+ +
+
+ +
+ +

+ Find Your Perfect +
+ + Mexico Destination + +

+ +

+ Answer 4 quick questions and we'll match you with your ideal vacation spot -- + plus get a free travel guide tailored to your style. +

+ + + +

Takes less than 60 seconds

+ + {/* Feature grid */} +
+ {[ + { icon: , label: '4 Destinations' }, + { icon: , label: 'Personalized' }, + { icon: , label: 'Free Guide' }, + ].map((f) => ( +
+
{f.icon}
+

{f.label}

+
+ ))} +
+
+
+ )} + + {/* Quiz Steps */} + {step === 'quiz' && ( +
+ {/* Progress Bar */} +
+
+ + + Question {currentQuestion + 1} of {QUESTIONS.length} + +
+
+
+
+
+ + {/* Question */} +
+
+

+ {QUESTIONS[currentQuestion].question} +

+

+ {QUESTIONS[currentQuestion].subtitle} +

+ +
+ {QUESTIONS[currentQuestion].options.map((option) => { + const isSelected = answers[QUESTIONS[currentQuestion].id] === option.value + return ( + + ) + })} +
+
+
+ + {/* Step dots */} +
+ {QUESTIONS.map((_, i) => ( +
+ ))} +
+
+ )} + + {/* Result Screen */} + {step === 'result' && result && ( +
+ {/* Result Hero */} +
+ {result.name} +
+
+
+

+ + Based on your answers... +

+

+ {result.name} +

+

{result.tagline}

+
+
+
+ + {/* Result Content */} +
+

+ {result.description} +

+ + {/* Highlights */} +
+ {result.highlights.map((h) => ( +
+ + {h} +
+ ))} +
+ + {/* Ebook Gate */} +
+

+ Get Your Personalized {result.name} Guide +

+

+ Our free "Budget Luxury Travel" guide includes insider tips + specific to {result.name} -- best times to visit, hidden gems, and how to + get 5-star experiences on a budget. +

+
+ +
+

+ Instant download -- no spam, ever. +

+
+ + {/* Pricing teaser */} +
+

Your {result.name} vacation starts at

+

+ ${PAYMENT_CONFIG.monthlyPrice}/mo +

+

+ 5 days, 4 nights all-inclusive -- or ${PAYMENT_CONFIG.oneTimePrice} one-time +

+
+ + {/* Testimonials */} +
+

+ What Other Travelers Say +

+ {TESTIMONIALS.slice(0, 3).map((t, i) => ( + + ))} +
+ + {/* PayNow Section */} +
+

+ Ready to Book {result.name}? +

+

+ Claim your all-inclusive vacation certificate now +

+ + +
+ + {/* Retake */} +
+ +
+
+ + + + {/* Footer */} + + + +
+ )} +
+ ) +} diff --git a/src/components/lp/pages/LP14SocialWall.tsx b/src/components/lp/pages/LP14SocialWall.tsx new file mode 100644 index 0000000..9310df6 --- /dev/null +++ b/src/components/lp/pages/LP14SocialWall.tsx @@ -0,0 +1,392 @@ +'use client' + +import { useState } from 'react' +import { Heart, MessageCircle, Send, Bookmark, MoreHorizontal, Camera, Users } from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import PricingDisplay from '@/components/lp/shared/PricingDisplay' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { TESTIMONIALS, PAYMENT_CONFIG } from '@/app/lp/_config/types' + +interface SocialPost { + id: number + username: string + avatar: string + image: string + caption: string + likes: number + comments: number + timeAgo: string + location: string +} + +const SOCIAL_POSTS: SocialPost[] = [ + { + id: 1, + username: 'sarahtravels_', + avatar: '/images/cdn/photo-1494790108377-be9c29b29330.jpg', + image: '/images/cdn/photo-1510097467424-192d713fd8b2.jpg', + caption: 'Still can\'t believe this was real life. Cancun you have my heart forever. Best $39/month I ever spent! #MexicoParadise #BudgetLuxury', + likes: 847, + comments: 43, + timeAgo: '2d', + location: 'Cancun, Mexico', + }, + { + id: 2, + username: 'mike.and.jen', + avatar: '/images/cdn/photo-1472099645785-5658abf4ff4e.jpg', + image: '/images/cdn/photo-1580415200778-625cb1890ab5.jpg', + caption: 'El Arco at sunset hits different when you know you paid less than $400 for the whole trip. The resort was 5-star quality! #CaboSanLucas #VacationCertificate', + likes: 1243, + comments: 67, + timeAgo: '3d', + location: 'Cabo San Lucas, Mexico', + }, + { + id: 3, + username: 'wanderlust.maria', + avatar: '/images/cdn/photo-1438761681033-6461ffad8d80.jpg', + image: '/images/cdn/photo-1518638150340-f706e86654de.jpg', + caption: 'Swimming in cenotes is a spiritual experience. The Riviera Maya exceeded every expectation. All meals included, all drinks included. This was the smartest travel decision I ever made.', + likes: 2104, + comments: 89, + timeAgo: '5d', + location: 'Riviera Maya, Mexico', + }, + { + id: 4, + username: 'dave_explores', + avatar: '/images/cdn/photo-1500648767791-00dcc994a43e.jpg', + image: '/images/cdn/photo-1585793753011-397e6e4668d6.jpg', + caption: 'Puerto Vallarta sunsets are unmatched. We extended 3 extra nights because we couldn\'t leave. The food alone was worth 10x what we paid. #PuertoVallarta #Sunset', + likes: 956, + comments: 38, + timeAgo: '1w', + location: 'Puerto Vallarta, Mexico', + }, + { + id: 5, + username: 'beach.rachel', + avatar: '/images/cdn/photo-1544005313-94ddf0286df2.jpg', + image: '/images/cdn/photo-1507525428034-b723cf961d3e.jpg', + caption: 'POV: You\'re paying $1.30/day for THIS. My friends thought I was joking when I told them the price. Nope, just smart travel planning. Download the free guide, seriously. #BudgetTravel', + likes: 3201, + comments: 156, + timeAgo: '4d', + location: 'Cancun, Mexico', + }, + { + id: 6, + username: 'james.patricia', + avatar: '/images/cdn/photo-1522529599102-193c0d76b5b6.jpg', + image: '/images/cdn/photo-1551882547-ff40c63fe5fa.jpg', + caption: 'The infinity pool at our resort in Cabo. All-inclusive means all-inclusive -- every cocktail, every meal, every sunset. We saved over $2,800 compared to booking directly. Not a typo.', + likes: 1678, + comments: 72, + timeAgo: '6d', + location: 'Cabo San Lucas, Mexico', + }, + { + id: 7, + username: 'travelwith.lisa', + avatar: '/images/cdn/photo-1494790108377-be9c29b29330.jpg', + image: '/images/cdn/photo-1581710862235-eb6e05d8783f.jpg', + caption: 'When people ask how we afford to travel so much... I just smile. The vacation certificate program changed everything for us. 5 nights for what most people pay for 1.', + likes: 2489, + comments: 104, + timeAgo: '1w', + location: 'Riviera Maya, Mexico', + }, + { + id: 8, + username: 'sunset.chris', + avatar: '/images/cdn/photo-1472099645785-5658abf4ff4e.jpg', + image: '/images/cdn/photo-1468413253725-0d5181091f76.jpg', + caption: 'Day 4 in Puerto Vallarta and I never want to leave. The Malecon at golden hour is pure magic. If you\'re still on the fence, just get the free guide -- you\'ll see. #GoldenHour', + likes: 1102, + comments: 51, + timeAgo: '3d', + location: 'Puerto Vallarta, Mexico', + }, +] + +function formatLikes(n: number): string { + if (n >= 1000) return `${(n / 1000).toFixed(1)}k` + return n.toString() +} + +function SocialPostCard({ post }: { post: SocialPost }) { + const [liked, setLiked] = useState(false) + const [saved, setSaved] = useState(false) + const displayLikes = liked ? post.likes + 1 : post.likes + + return ( +
+ {/* Header */} +
+
+ {post.username} +
+

{post.username}

+

{post.location}

+
+
+ +
+ + {/* Image */} + {post.caption} + + {/* Action buttons */} +
+
+ + + +
+ +
+ + {/* Likes & Caption */} +
+

+ {formatLikes(displayLikes)} likes +

+

+ {post.username}{' '} + {post.caption} +

+

+ View all {post.comments} comments -- {post.timeAgo} ago +

+
+
+ ) +} + +export default function LP14SocialWall() { + return ( +
+ + + {/* Hero */} +
+
+
+
+ + #MexicoParadise +
+ +

+ Join 2,847 +
+ Happy Travelers +

+ +

+ Real people. Real vacations. Real savings. See what our travelers + are posting from their all-inclusive Mexico getaways. +

+ +
+ +
+ + 12,400+ downloads +
+
+ + {/* Traveler avatars */} +
+
+ {TESTIMONIALS.slice(0, 5).map((t, i) => ( + {t.name} + ))} +
+ + + 2,842 more travelers + +
+
+
+ + {/* Social Post Grid */} +
+

+ Straight from Their Feeds +

+

+ What travelers are sharing about their Mexico Paradise experience +

+ +
+ {SOCIAL_POSTS.slice(0, 6).map((post) => ( + + ))} +
+
+ + {/* Stats Bar */} +
+
+ {[ + { value: '2,847', label: 'Happy Travelers' }, + { value: '4.8/5', label: 'Average Rating' }, + { value: '$2,800+', label: 'Avg. Savings' }, + { value: '18 mo', label: 'Booking Window' }, + ].map((stat) => ( +
+

{stat.value}

+

{stat.label}

+
+ ))} +
+
+ + {/* More posts */} +
+
+ {SOCIAL_POSTS.slice(6).map((post) => ( + + ))} +
+
+ + {/* Ebook Capture */} +
+
+
+ +
+

+ Your Turn to Post +

+

+ Get our free "Budget Luxury Travel" guide and discover 5 secrets + to luxury Mexico vacations on a budget. Your feed is about to level up. +

+ +
+
+ + {/* Testimonials */} +
+

+ Verified Reviews +

+
+ {TESTIMONIALS.slice(0, 3).map((t, i) => ( + + ))} +
+
+ + {/* PayNow Section */} +
+
+

+ Join the Community +

+

+ 5 days, 4 nights all-inclusive in Mexico. + Starting at just ${PAYMENT_CONFIG.monthlyPrice}/month. +

+ + + + + + +
+
+ + + + {/* Footer */} + + + +
+ ) +} diff --git a/src/components/lp/pages/LP15SavingsJournal.tsx b/src/components/lp/pages/LP15SavingsJournal.tsx new file mode 100644 index 0000000..6b7dda6 --- /dev/null +++ b/src/components/lp/pages/LP15SavingsJournal.tsx @@ -0,0 +1,465 @@ +'use client' + +import { DollarSign, TrendingDown, Coffee, Plane, PiggyBank, Calculator, ArrowDown, Check } from 'lucide-react' +import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import PricingDisplay from '@/components/lp/shared/PricingDisplay' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { TESTIMONIALS, PAYMENT_CONFIG } from '@/app/lp/_config/types' + +const DAILY_COMPARISONS = [ + { label: 'Morning Latte', cost: 5.50, icon: Coffee, color: '#795548' }, + { label: 'Fast Food Lunch', cost: 12.00, icon: DollarSign, color: '#F44336' }, + { label: 'Streaming Subs', cost: 4.30, icon: DollarSign, color: '#9C27B0' }, + { label: 'Mexico Vacation', cost: 1.30, icon: Plane, color: '#2E7D32' }, +] + +const SAVINGS_CHART_DATA = [ + { name: 'Booking Direct', cost: 3200, fill: '#EF5350' }, + { name: 'Travel Agent', cost: 2800, fill: '#FF7043' }, + { name: 'Online Deal', cost: 2100, fill: '#FFA726' }, + { name: 'Certificate', cost: 399, fill: '#2E7D32' }, +] + +const COST_BREAKDOWN = [ + { item: 'Hotel (4 nights)', regular: 1600, certificate: 0 }, + { item: 'All meals', regular: 600, certificate: 0 }, + { item: 'Drinks', regular: 300, certificate: 0 }, + { item: 'Resort amenities', regular: 200, certificate: 0 }, + { item: 'Certificate cost', regular: 0, certificate: 399 }, +] + +export default function LP15SavingsJournal() { + const totalRegular = COST_BREAKDOWN.reduce((sum, item) => sum + item.regular, 0) + const totalCertificate = COST_BREAKDOWN.reduce((sum, item) => sum + item.certificate, 0) + const savings = totalRegular - totalCertificate + + return ( +
+ + + {/* Hero */} +
+ {/* Decorative pattern */} +
+ +
+
+ + + The Savings Calculator + +
+ +

+ The Numbers That +
+ Will Surprise You +

+ +

+ A 5-star Mexico vacation for less than your daily coffee habit. + Let's break down the math. +

+ + {/* Daily cost hero stat */} +
+

Your daily vacation cost

+
+ + $1.30 + + /day +
+

+ That's less than a morning latte. +

+ +
+
+
+ + {/* Daily Cost Comparison */} +
+

+ What $1.30 a Day Looks Like +

+

+ Things you spend more on every day without thinking twice +

+ +
+ {DAILY_COMPARISONS.map((item) => { + const IconComponent = item.icon + const isVacation = item.label === 'Mexico Vacation' + return ( +
+
+ +
+
+

{item.label}

+

+ ${item.cost.toFixed(2)}/day +

+
+ {isVacation && ( + + BEST + + )} +
+ ) + })} +
+ +
+ +

+ Skip your latte 4 days a month and your vacation pays for itself. +

+

+ 4 lattes = $22.00 -- that's more than half a monthly payment of ${PAYMENT_CONFIG.monthlyPrice}. +

+
+
+ + {/* Savings Chart */} +
+
+

+ How Booking Methods Compare +

+

+ Average cost for 5 nights all-inclusive Mexico vacation (per couple) +

+ +
+
+ + + `$${value}`} + axisLine={false} + tickLine={false} + tick={{ fontSize: 12, fill: '#9CA3AF' }} + /> + + [`$${value}`, 'Cost']} + contentStyle={{ + borderRadius: '8px', + border: '1px solid #E5E7EB', + boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)', + }} + /> + + + +
+ +
+ +

+ Certificate holders save an average of $2,801 per vacation +

+
+
+
+
+ + {/* Cost Breakdown Table */} +
+

+ The Full Breakdown +

+

+ What's included in your certificate vs. paying full price +

+ +
+ + + + + + + + + + {COST_BREAKDOWN.map((row) => ( + + + + + + ))} + + + + + + + + + + + + +
ItemRegular PriceCertificate
{row.item} + {row.regular > 0 ? `$${row.regular.toLocaleString()}` : '--'} + + {row.certificate > 0 ? `$${row.certificate}` : ( + + + Included + + )} +
Total + ${totalRegular.toLocaleString()} + + ${totalCertificate} +
+ You save + + ${savings.toLocaleString()} +
+
+
+ + {/* Ebook Capture */} +
+
+
+ +
+

+ Get the Full Savings Guide +

+

+ Our free "Budget Luxury Travel" guide reveals 5 more strategies to + maximize your savings -- including a trick that can save you up to $500 extra + on your trip. +

+
+ +
+

+ Join 12,400+ smart travelers who downloaded this guide +

+
+
+ + {/* Payment option highlight */} +
+

+ Two Ways to Save +

+

+ Choose the payment plan that fits your budget +

+ +
+ {/* Monthly */} +
+

Monthly Plan

+

+ ${PAYMENT_CONFIG.monthlyPrice}/mo +

+

+ for {PAYMENT_CONFIG.totalMonths} months (${PAYMENT_CONFIG.totalPrice} total) +

+

+ That's just ${(PAYMENT_CONFIG.monthlyPrice / 30).toFixed(2)}/day +

+
+ Most Popular +
+
+ + {/* One-time */} +
+

One-Time Payment

+

+ ${PAYMENT_CONFIG.oneTimePrice} +

+

+ single payment -- done! +

+

+ That's ${(PAYMENT_CONFIG.oneTimePrice / 365).toFixed(2)}/day over a year +

+
+ Best Value +
+
+
+
+ + {/* Testimonials */} +
+

+ Smart Travelers, Happy Reviews +

+
+ {TESTIMONIALS.slice(0, 4).map((t, i) => ( + + ))} +
+
+ + {/* PayNow Section */} +
+
+

+ Start Saving Today +

+

+ Claim your 5-day, 4-night all-inclusive Mexico vacation certificate +

+ + + + +
+
+ + + + {/* FAQ */} +
+

+ Questions About Pricing & Value +

+ +
+ + {/* Footer */} + + + +
+ ) +} diff --git a/src/components/lp/pages/LP16CouplesRetreat.tsx b/src/components/lp/pages/LP16CouplesRetreat.tsx new file mode 100644 index 0000000..d5e7e0c --- /dev/null +++ b/src/components/lp/pages/LP16CouplesRetreat.tsx @@ -0,0 +1,445 @@ +'use client' + +import { useState } from 'react' +import { Heart, Sparkles, Wine, Sunset, Star, MapPin, Gift, Music } from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import DestinationCarousel from '@/components/lp/shared/DestinationCarousel' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const ROSE = '#880E4F' +const WINE_COLOR = '#4A0025' +const GOLD = '#D4AF37' + +const couplesTestimonials = [ + { + quote: "We renewed our vows on the beach at sunset. This trip brought us closer together than we've been in years.", + name: "Sarah & Mike", + location: "Chicago, IL", + photo: "/images/cdn/photo-1522529599102-193c0d76b5b6.jpg", + }, + { + quote: "Our anniversary trip to Cabo was pure magic. Candlelit dinners, couples massages, and the most breathtaking views together.", + name: "Jennifer & Tom", + location: "New York, NY", + photo: "/images/cdn/photo-1494790108377-be9c29b29330.jpg", + }, + { + quote: "We fell in love all over again in the Riviera Maya. The romance package made everything feel so special and intimate.", + name: "David & Lisa", + location: "Denver, CO", + photo: "/images/cdn/photo-1472099645785-5658abf4ff4e.jpg", + }, +] + +const romanticBenefits = [ + { + icon: Wine, + title: "Private Candlelit Dinners", + description: "Dine together under the stars with gourmet cuisine and premium wines included in your all-inclusive stay.", + }, + { + icon: Sparkles, + title: "Couples Spa Experiences", + description: "Side-by-side massages, aromatherapy baths, and relaxation rituals designed for the two of you.", + }, + { + icon: Sunset, + title: "Sunset Beach Walks", + description: "Miles of pristine shoreline reserved for your private moments together as the sky paints itself gold.", + }, + { + icon: Music, + title: "Live Music & Dancing", + description: "Sway together to live Latin rhythms at the resort's intimate lounges and open-air terraces.", + }, + { + icon: MapPin, + title: "Romantic Excursions", + description: "Snorkeling together in crystal cenotes, sailing at sunset, or exploring ancient ruins hand in hand.", + }, + { + icon: Gift, + title: "Special Touches", + description: "Rose petal turndowns, champagne on arrival, and little surprises that make your getaway unforgettable.", + }, +] + +export default function LP16CouplesRetreat() { + const [showAllBenefits, setShowAllBenefits] = useState(false) + + return ( +
+ + + {/* Hero Section */} +
+
+ Romantic couple on beach at sunset +
+
+
+ +
+
+ + + A Romantic Escape for Two + + +
+ +

+ You Both Deserve This +

+ +

+ 5 days and 4 nights at an all-inclusive Mexican paradise. + Just the two of you, the ocean, and nothing on your calendar. +

+ +

+ Starting at just ${PAYMENT_CONFIG.monthlyPrice}/month together +

+ +
+

+ Get our free couples travel guide first: +

+ +
+
+ + {/* Decorative candlelight glow */} +
+
+ + {/* What Awaits You Both */} +
+
+
+

+ Your Romantic Itinerary +

+

+ What Awaits You Both +

+

+ Every detail of your escape is designed for connection, relaxation, and romance. +

+
+ +
+ {romanticBenefits + .slice(0, showAllBenefits ? undefined : 3) + .map((benefit) => ( +
+ +

+ {benefit.title} +

+

+ {benefit.description} +

+
+ ))} +
+ + {!showAllBenefits && ( +
+ +
+ )} +
+
+ + {/* Side-by-side image + quote */} +
+
+
+ Romantic resort setting +
+
+
+ +
+ “The best thing we ever did for our relationship was stop saying + ‘someday’ and book the trip.” +
+

+ — Every couple who finally went +

+
+
+
+
+ + {/* Destination Carousel */} +
+
+
+

+ Choose Your Escape Together +

+

+ Four Romantic Destinations +

+
+ + +
+
+ + {/* Ebook Capture Section */} +
+
+ +

+ Free Couples Travel Guide +

+

+ Discover 5 secrets to planning a luxury Mexico vacation together — + without the luxury price tag. Written for couples, by couples. +

+ + + +

+ Join 12,000+ couples who downloaded our guide +

+
+
+ + {/* Pricing + Pay Now */} +
+
+
+

+ Your Romantic Getaway +

+

+ Ready to Go Together? +

+

+ 5 days, 4 nights, all-inclusive — for both of you. +

+
+ +
+
+ + ${PAYMENT_CONFIG.monthlyPrice} + + /mo for {PAYMENT_CONFIG.totalMonths} months +

+ or ${PAYMENT_CONFIG.oneTimePrice} one-time +

+
+ + +
+ + +
+
+ + {/* Couples Testimonials */} +
+
+
+

+ Love Stories from Paradise +

+
+ +
+ {couplesTestimonials.map((t) => ( + + ))} +
+
+
+ + + + {/* FAQ */} +
+
+

+ Questions Couples Ask +

+ + +
+
+ + {/* Final CTA */} +
+ +

+ Your Love Story Deserves a Beautiful Setting +

+

+ Start with our free guide. Dream together tonight, travel together soon. +

+
+ +
+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP17Postcards.tsx b/src/components/lp/pages/LP17Postcards.tsx new file mode 100644 index 0000000..703bfc0 --- /dev/null +++ b/src/components/lp/pages/LP17Postcards.tsx @@ -0,0 +1,535 @@ +'use client' + +import { useState } from 'react' +import { Plane, Send, MapPin, Stamp } from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, DESTINATIONS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const BROWN = '#5D4037' +const AIRMAIL_BLUE = '#1565C0' +const RED = '#D32F2F' +const CREAM = '#FFF8E1' +const PAPER = '#FFFDF5' + +interface PostcardData { + destination: string + image: string + message: string + stamp: string + dateline: string +} + +const postcards: PostcardData[] = [ + { + destination: 'Cancun', + image: '/images/cdn/photo-1510097467424-192d713fd8b2.jpg', + message: "Dear Future You,\n\nThe water here is the most unreal shade of turquoise. We spent all day at the pool bar and didn't spend a dime — everything's included! Tomorrow we're snorkeling. Wish you were here already.\n\nWith love from paradise,\nYour Future Self", + stamp: 'MX', + dateline: 'Cancun, Mexico', + }, + { + destination: 'Cabo San Lucas', + image: '/images/cdn/photo-1580415200778-625cb1890ab5.jpg', + message: "Querido amigo,\n\nThe arch at Land's End is even more stunning in person. Had the best fish tacos of my life today, and the sunset from our balcony — I can't even describe it. Why didn't we come sooner?\n\nNever leaving,\nYour Happy Self", + stamp: 'MX', + dateline: 'Cabo San Lucas, Mexico', + }, + { + destination: 'Riviera Maya', + image: '/images/cdn/photo-1518638150340-f706e86654de.jpg', + message: "Hey there,\n\nSwam in a cenote today. Underground. Crystal clear water surrounded by ancient limestone. Then explored Mayan ruins. This place is pure magic — history and paradise mixed together.\n\nCome see for yourself,\nYour Adventurous Side", + stamp: 'MX', + dateline: 'Riviera Maya, Mexico', + }, + { + destination: 'Puerto Vallarta', + image: '/images/cdn/photo-1585793753011-397e6e4668d6.jpg', + message: "Hola from PV!\n\nWalked the Malecon at sunset. Street musicians, amazing art, and the most gorgeous views of the bay. Had dinner at a rooftop restaurant — all included! This town has so much soul.\n\nSending sunshine,\nThe Relaxed You", + stamp: 'MX', + dateline: 'Puerto Vallarta, Mexico', + }, +] + +function PostcardCard({ postcard }: { postcard: PostcardData }) { + const [flipped, setFlipped] = useState(false) + + return ( +
setFlipped(!flipped)} + style={{ perspective: '1000px' }} + > +
+ {/* Front — Photo side */} +
+ {/* Airmail border */} +
+ +
+ {postcard.destination} +
+ +
+
+
+

+ {postcard.destination} +

+

+ {postcard.dateline} +

+
+ + Flip me! + +
+
+
+ + {/* Back — Message side */} +
+
+

+ {postcard.dateline} +

+

+ {postcard.message} +

+
+ +
+ + Click to flip back + +
+
+ + + {postcard.stamp} + +
+
+
+
+
+
+ ) +} + +export default function LP17Postcards() { + return ( +
+ + + {/* Hero */} +
+
+ Beautiful Mexico coastline +
+
+ +
+ {/* Airmail decoration */} +
+
+ +
+
+ +

+ Wish You Were Here +

+ +

+ Soon you will be... +

+ +

+ 5 days, 4 nights, all-inclusive at a luxury Mexican resort. + Starting at just ${PAYMENT_CONFIG.monthlyPrice}/month. + Your next postcard writes itself. +

+ +
+

+ Start planning with our free travel guide: +

+ +
+
+
+ + {/* Postcard Collection */} +
+
+
+ +

+ Postcards from Paradise +

+

+ Click each postcard to read the message on the back +

+
+ +
+ {postcards.map((postcard) => ( + + ))} +
+
+
+ + {/* What's Included — styled like a travel itinerary */} +
+
+
+

+ Your Travel Itinerary +

+
+ +
+
+ + + MEXICO PARADISE VACATIONS + +
+ + {[ + { day: 'Included', item: '5 Days / 4 Nights at a luxury all-inclusive resort' }, + { day: 'Included', item: 'All meals — breakfast, lunch, dinner, and snacks' }, + { day: 'Included', item: 'Unlimited drinks — cocktails, beer, wine, soft drinks' }, + { day: 'Included', item: 'Resort pools, beaches, and amenities' }, + { day: 'Included', item: 'Your choice of 4 stunning destinations' }, + { day: 'Included', item: '18 months to book your travel dates' }, + ].map((item, i) => ( +
+ + {item.day} + + {item.item} +
+ ))} + +
+ + TOTAL COST + +
+ + $1,500+ + + + ${PAYMENT_CONFIG.monthlyPrice}/mo + +
+
+
+
+
+ + {/* Ebook Section */} +
+
+ +

+ Free: Budget Luxury Travel Guide +

+

+ 5 secrets to luxury Mexico vacations on a budget +

+

+ Your first class ticket to smarter travel planning +

+ + +
+
+ + {/* Pay Now Section */} +
+
+
+

+ Ready to Send Your Own Postcard? +

+

+ Book your all-inclusive Mexico vacation today +

+
+ +
+
+ + ${PAYMENT_CONFIG.monthlyPrice} + + /mo for {PAYMENT_CONFIG.totalMonths} months +

+ or ${PAYMENT_CONFIG.oneTimePrice} one-time +

+
+ + +
+ + +
+
+ + + + {/* FAQ */} +
+
+

+ Frequently Asked Questions +

+ + +
+
+ + {/* Final CTA */} +
+

+ Wish you were here? +

+

+ Soon You Will Be. +

+
+ +
+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP18StressRelief.tsx b/src/components/lp/pages/LP18StressRelief.tsx new file mode 100644 index 0000000..907e6b4 --- /dev/null +++ b/src/components/lp/pages/LP18StressRelief.tsx @@ -0,0 +1,464 @@ +'use client' + +import { Leaf, Waves, Sun, CloudSun, Heart, Wind, Droplets, TreePine } from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, DESTINATIONS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const TEAL = '#004D40' +const MINT = '#E0F2F1' +const SOFT_TEAL = '#00796B' +const LIGHTEST = '#F1F8F7' + +const stressStats = [ + { stat: '77%', label: 'of Americans report physical symptoms of stress regularly' }, + { stat: '48%', label: 'say stress has increased in the past 5 years' }, + { stat: '1 in 3', label: 'haven\'t taken a vacation in over 2 years' }, +] + +const wellnessBenefits = [ + { + icon: Waves, + title: 'Ocean Therapy', + description: 'The sound of waves naturally lowers cortisol levels. Your resort sits steps from the shore.', + }, + { + icon: Sun, + title: 'Vitamin D Reset', + description: 'Sunshine boosts serotonin production, helping restore your natural sleep-wake cycle.', + }, + { + icon: Wind, + title: 'Digital Detox', + description: 'No deadlines, no meetings, no notifications. Five days of being truly present.', + }, + { + icon: Droplets, + title: 'Spa & Wellness', + description: 'On-site spa facilities with massage, hydrotherapy, and relaxation areas included in your stay.', + }, + { + icon: Leaf, + title: 'Nature Immersion', + description: 'Tropical gardens, cenotes, and jungle paths — nature is the original stress reliever.', + }, + { + icon: Heart, + title: 'Connection', + description: 'Uninterrupted time with the people who matter most. No rushing, no agenda.', + }, +] + +const reliefTestimonials = [ + { + quote: "I didn't realize how burned out I was until day two when I finally stopped thinking about work. By day four, I felt like a different person. I actually cried happy tears.", + name: "Maria G.", + location: "Houston, TX", + photo: "/images/cdn/photo-1438761681033-6461ffad8d80.jpg", + }, + { + quote: "My therapist told me to take a real vacation. This was it. The sound of the ocean, no phone, good food — I came back genuinely rested for the first time in years.", + name: "Rachel T.", + location: "Phoenix, AZ", + photo: "/images/cdn/photo-1544005313-94ddf0286df2.jpg", + }, + { + quote: "We both work stressful jobs. This trip was medicine. Waking up without an alarm, eating breakfast overlooking the ocean — we needed every second of it.", + name: "David & Lisa", + location: "Denver, CO", + photo: "/images/cdn/photo-1472099645785-5658abf4ff4e.jpg", + }, +] + +export default function LP18StressRelief() { + return ( +
+ + + {/* Hero — Serene, minimal */} +
+
+ Peaceful beach meditation at sunrise +
+
+ +
+ + +

+ Your Mind Needs
+ a Beach +

+ +

+ Five days of warm sand, gentle waves, and absolutely nothing + you have to do. All-inclusive. All taken care of. +

+ +

+ From ${PAYMENT_CONFIG.monthlyPrice}/month · No rush, no pressure +

+ +
+

+ Start with a free guide to planning your escape: +

+ +
+
+
+ + {/* Breathing space */} +
+ + {/* Why You Need This */} +
+
+

+ Why You Need This Escape +

+ +

+ You already know. The tension in your shoulders. The racing thoughts at 2 AM. + The feeling that you are always behind on something. Your body and mind are + asking for a pause. This is that pause. +

+ +
+ {stressStats.map((item) => ( +
+

+ {item.stat} +

+

+ {item.label} +

+
+ ))} +
+
+
+ + {/* Breathing space */} +
+ + {/* Wellness Benefits */} +
+
+
+

+ How This Trip Heals +

+

+ Science-backed reasons why vacation is medicine +

+
+ +
+ {wellnessBenefits.map((benefit) => ( +
+
+ +
+
+

+ {benefit.title} +

+

+ {benefit.description} +

+
+
+ ))} +
+
+
+ + {/* Peaceful image break */} +
+ Serene ocean view +
+

+ “Almost everything will work again if you unplug it for a few minutes—including you.” +

+
+
+ + {/* Destinations — gentle presentation */} +
+
+
+

+ Four Places to Find Your Peace +

+
+ +
+ {DESTINATIONS.map((dest) => ( +
+
+ {dest.name} +
+
+

+ {dest.name} +

+

+ {dest.tagline} +

+
+
+ ))} +
+
+
+ + {/* Ebook Section */} +
+
+ +

+ Your Free Travel Guide +

+

+ Budget Luxury Travel: 5 secrets to luxury Mexico vacations on a budget +

+

+ No sales pitch. Just helpful information to start dreaming. +

+ + +
+
+ + {/* Gentle Pay Now */} +
+
+
+

+ When You Are Ready +

+

+ 5 days, 4 nights, all-inclusive. Take your time deciding. +

+
+ +
+
+ + ${PAYMENT_CONFIG.monthlyPrice} + + /month for {PAYMENT_CONFIG.totalMonths} months +

+ or ${PAYMENT_CONFIG.oneTimePrice} one-time · 30-day refund guarantee +

+
+ + +
+ + +
+
+ + {/* Testimonials */} +
+
+
+

+ They Came Back Renewed +

+
+ +
+ {reliefTestimonials.map((t) => ( + + ))} +
+
+
+ + + + {/* FAQ */} +
+
+

+ Common Questions +

+ + +
+
+ + {/* Gentle final CTA */} +
+ +

+ Give Yourself Permission to Rest +

+

+ Start with the free guide. No commitment, no rush. + Just a first step toward the break you deserve. +

+
+ +
+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP19FoodieParadise.tsx b/src/components/lp/pages/LP19FoodieParadise.tsx new file mode 100644 index 0000000..1988398 --- /dev/null +++ b/src/components/lp/pages/LP19FoodieParadise.tsx @@ -0,0 +1,496 @@ +'use client' + +import { UtensilsCrossed, Wine, Coffee, IceCream, Flame, ChefHat, GlassWater, Beef } from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, FAQ_ITEMS } from '@/app/lp/_config/types' + +const CHILE_RED = '#BF360C' +const CHOCOLATE = '#3E2723' +const AMBER = '#FF8F00' +const CREAM = '#FFF8E1' +const DARK_BG = '#1A0F0A' + +interface MenuItem { + name: string + description: string + tag?: string +} + +interface MenuSection { + title: string + icon: React.ElementType + items: MenuItem[] +} + +const menuSections: MenuSection[] = [ + { + title: 'Breakfast Buffet', + icon: Coffee, + items: [ + { name: 'Chilaquiles Verdes', description: 'Crispy tortillas in tangy tomatillo salsa, crema, queso fresco, and fried eggs', tag: 'Chef\'s Pick' }, + { name: 'Tropical Fruit Station', description: 'Fresh mango, papaya, pineapple, dragon fruit, and coconut' }, + { name: 'Huevos Rancheros', description: 'Farm eggs on corn tortillas with ranchero sauce, refried beans, and avocado' }, + { name: 'Made-to-Order Omelettes', description: 'Choose your fillings: peppers, mushrooms, chorizo, Oaxaca cheese' }, + ], + }, + { + title: 'Poolside Lunch', + icon: GlassWater, + items: [ + { name: 'Baja Fish Tacos', description: 'Beer-battered mahi-mahi, chipotle crema, mango pico, shredded cabbage', tag: 'Fan Favorite' }, + { name: 'Ceviche Trio', description: 'Shrimp, octopus, and fish ceviche with avocado, lime, and tostadas' }, + { name: 'Grilled Lobster Quesadilla', description: 'Butter-poached lobster, Oaxaca cheese, roasted corn salsa' }, + { name: 'Guacamole Fresco', description: 'Tableside-prepared with Hass avocados, serrano chile, cilantro, lime' }, + ], + }, + { + title: 'Dinner Grill', + icon: Flame, + items: [ + { name: 'Surf & Turf Mexicano', description: 'Grilled ribeye with chimichurri and garlic butter shrimp', tag: 'Signature' }, + { name: 'Cochinita Pibil', description: 'Slow-roasted Yucatan pork in achiote, pickled red onion, habanero' }, + { name: 'Mole Negro', description: 'Heritage recipe with 28 ingredients, served over free-range chicken' }, + { name: 'Whole Grilled Red Snapper', description: 'Al pastor-seasoned, grilled over charcoal with roasted vegetables' }, + ], + }, + { + title: 'All-Day Bar', + icon: Wine, + items: [ + { name: 'Premium Margaritas', description: 'Classic lime, mango habanero, tamarind, hibiscus, spicy watermelon', tag: 'Unlimited' }, + { name: 'Mexican Craft Beer', description: 'Rotating selection of local craft breweries from across Mexico' }, + { name: 'Fresh Juice Bar', description: 'Cold-pressed juices, smoothies, and agua frescas made to order' }, + { name: 'Top-Shelf Spirits', description: 'Premium tequila, mezcal, rum, whiskey — all included in your stay' }, + ], + }, +] + +const diningVenues = [ + { + name: 'Oceanfront Grill', + description: 'Seafood and steaks with your toes in the sand', + image: '/images/cdn/photo-1414235077428-338989a2e8c0.jpg', + }, + { + name: 'La Hacienda', + description: 'Authentic regional Mexican cuisine in a colonial courtyard', + image: '/images/cdn/photo-1555396273-367ea4eb4db5.jpg', + }, + { + name: 'Teppanyaki Live', + description: 'Japanese-Mexican fusion with live tableside cooking', + image: '/images/cdn/photo-1517248135467-4c7edcad34c4.jpg', + }, + { + name: 'Dolce Vita', + description: 'Italian-inspired dishes with a Mexican twist', + image: '/images/cdn/photo-1550966871-3ed3cdb51f3a.jpg', + }, +] + +export default function LP19FoodieParadise() { + return ( +
+ + + {/* Hero */} +
+
+ Gourmet Mexican cuisine spread +
+
+ +
+
+ + + All-Inclusive Dining + + +
+ +

+ Unlimited Everything +

+ +

+ 5 days, 4 nights of all-you-can-eat gourmet cuisine, unlimited premium drinks, + and world-class dining at a luxury Mexican resort. +

+ +

+ From ${PAYMENT_CONFIG.monthlyPrice}/month · Every meal, every drink, every bite — included. +

+ +
+

+ Get our free travel guide to eating your way through Mexico: +

+ +
+
+
+ + {/* The Menu */} +
+
+
+ +

+ The Menu +

+

+ A taste of what awaits — all included in your stay +

+
+
+ + {menuSections.map((section, sIndex) => ( +
+
+ +

+ {section.title} +

+
+
+ +
+ {section.items.map((item) => ( +
+
+
+ + {item.name} + + {item.tag && ( + + {item.tag} + + )} +
+

+ {item.description} +

+
+ + Included + +
+ ))} +
+ + {sIndex < menuSections.length - 1 && ( +
+ )} +
+ ))} + +
+

+ Plus dessert buffets, 24-hour room service, late-night snack bars, and more. +

+

+ All included. Eat as much as you want. +

+
+
+
+ + {/* Dining Venues */} +
+
+
+

+ Multiple Restaurants, One Resort +

+

+ No reservations needed. No checks at the end of the meal. +

+
+ +
+ {diningVenues.map((venue) => ( +
+
+ {venue.name} +
+
+

+ {venue.name} +

+

+ {venue.description} +

+
+
+ ))} +
+
+
+ + {/* All You Can Eat emphasis */} +
+
+ +

+ All You Can Eat & Drink +

+

+ Breakfast. Lunch. Dinner. Snacks. Cocktails. Premium liquor. Craft beer. + Fresh juice. Coffee. Room service. Every single bite and sip is included + in your certificate price. No hidden costs. No resort fees. No surprise bar tabs. +

+ +
+ {[ + { icon: Coffee, label: 'Breakfast Buffet' }, + { icon: UtensilsCrossed, label: 'Multi-Course Dinners' }, + { icon: Wine, label: 'Premium Bar' }, + { icon: IceCream, label: 'Dessert & Snacks' }, + ].map((item) => ( +
+ +

+ {item.label} +

+
+ ))} +
+
+
+ + {/* Ebook Section */} +
+
+ +

+ Free: Budget Luxury Travel Guide +

+

+ 5 secrets to luxury Mexico vacations on a budget +

+

+ Including insider tips on the best resort dining experiences +

+ + +
+
+ + {/* Pay Now */} +
+
+
+

+ Ready to Feast? +

+

+ 5 days, 4 nights, unlimited food and drink. +

+
+ +
+
+ + ${PAYMENT_CONFIG.monthlyPrice} + + /mo for {PAYMENT_CONFIG.totalMonths} months +

+ or ${PAYMENT_CONFIG.oneTimePrice} one-time · Every meal included +

+
+ + +
+ + +
+
+ + + + {/* FAQ */} +
+
+

+ Frequently Asked Questions +

+ + +
+
+ + {/* Final CTA */} +
+ +

+ Your Table Is Waiting +

+

+ Start with the free guide. Then come hungry. +

+
+ +
+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP20FamilyEscape.tsx b/src/components/lp/pages/LP20FamilyEscape.tsx new file mode 100644 index 0000000..76d04d8 --- /dev/null +++ b/src/components/lp/pages/LP20FamilyEscape.tsx @@ -0,0 +1,532 @@ +'use client' + +import { + Sun, Waves, TreePalm, Gamepad2, Shield, Heart, + Umbrella, IceCream, Music, Fish, Castle, Palette, + Coffee, Sparkles, Users, Star, MapPin, +} from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, DESTINATIONS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const OCEAN_BLUE = '#0277BD' +const ORANGE = '#FF6F00' +const GREEN = '#7CB342' +const LIGHT_BLUE = '#E1F5FE' +const SAND = '#FFF8E1' + +const kidsGet = [ + { icon: Waves, text: 'Splash parks and kid-friendly pools' }, + { icon: Castle, text: 'Kids club with supervised activities' }, + { icon: IceCream, text: 'Unlimited ice cream and snacks' }, + { icon: Gamepad2, text: 'Beach games, sandcastle contests' }, + { icon: Fish, text: 'Snorkeling in shallow, calm waters' }, + { icon: Palette, text: 'Arts & crafts and treasure hunts' }, +] + +const parentsGet = [ + { icon: Coffee, text: 'Quiet adults-only pool and lounge' }, + { icon: Sparkles, text: 'Spa treatments and massage' }, + { icon: Music, text: 'Evening entertainment and live shows' }, + { icon: Sun, text: 'Beach time without checking the clock' }, + { icon: Heart, text: 'Date nights while kids are at the club' }, + { icon: Umbrella, text: 'All meals handled — no cooking, no dishes' }, +] + +const familyActivities = [ + { + title: 'Beach Adventures', + description: 'Build sandcastles, swim in warm turquoise waters, and spot tropical fish together.', + image: '/images/cdn/photo-1507525428034-b723cf961d3e.jpg', + }, + { + title: 'Snorkeling for All Ages', + description: 'Calm, shallow reefs perfect for first-time snorkelers. Equipment provided for the whole family.', + image: '/images/cdn/photo-1544551763-46a013bb70d5.jpg', + }, + { + title: 'Pool Party Every Day', + description: 'Waterslides, splash pads, and a swim-up bar (juice for the kids, cocktails for you).', + image: '/images/cdn/photo-1576610616656-d3aa5d1f4534.jpg', + }, + { + title: 'Cultural Exploration', + description: 'Visit ancient Mayan ruins, local markets, and learn about Mexican culture as a family.', + image: '/images/cdn/photo-1518638150340-f706e86654de.jpg', + }, +] + +const familyTestimonials = [ + { + quote: "Our kids still talk about this trip every single day. The kids club was amazing — they didn't want to leave! Meanwhile, we got actual relaxation time.", + name: "Sarah & Mike", + location: "Chicago, IL", + photo: "/images/cdn/photo-1522529599102-193c0d76b5b6.jpg", + }, + { + quote: "Best family vacation ever. Not having to worry about meal costs with three hungry kids was a game-changer. Everything was included!", + name: "Jennifer & Tom", + location: "New York, NY", + photo: "/images/cdn/photo-1494790108377-be9c29b29330.jpg", + }, + { + quote: "The kids learned to snorkel, built a hundred sandcastles, and made friends from all over. We actually came back rested — as parents! That never happens.", + name: "James & Patricia", + location: "Miami, FL", + photo: "/images/cdn/photo-1500648767791-00dcc994a43e.jpg", + }, +] + +export default function LP20FamilyEscape() { + return ( +
+ + + {/* Hero */} +
+
+ Beautiful family beach vacation +
+
+ +
+
+ + + +
+ +

+ Give Them the Vacation +
+ + They've Been Asking For + +

+ +

+ 5 days, 4 nights at an all-inclusive Mexican resort. + Unlimited fun for the kids. Actual relaxation for you. +

+ +

+ Starting at just ${PAYMENT_CONFIG.monthlyPrice}/month for the whole family +

+ +
+

+ Get our free family travel planning guide: +

+ +
+
+ + {/* Playful wave divider */} +
+ + + +
+
+ + {/* Kids Get / Parents Get Split */} +
+
+
+

+ Something for Everyone +

+

+ The whole family wins on this vacation +

+
+ +
+ {/* Kids Column */} +
+
+
+ +
+

+ Kids Get... +

+
+ +
+ {kidsGet.map((item) => ( +
+
+ +
+ + {item.text} + +
+ ))} +
+ +
+

+ “This is the BEST vacation EVER!” +

+

+ — Every kid who visits +

+
+
+ + {/* Parents Column */} +
+
+
+ +
+

+ Parents Get... +

+
+ +
+ {parentsGet.map((item) => ( +
+
+ +
+ + {item.text} + +
+ ))} +
+ +
+

+ “We actually came back rested!” +

+

+ — Every parent who visits +

+
+
+
+
+
+ + {/* Family Activities */} +
+
+
+

+ Adventures the Whole Family Will Love +

+

+ Create memories that last a lifetime +

+
+ +
+ {familyActivities.map((activity) => ( +
+
+ {activity.title} +
+
+

+ {activity.title} +

+

+ {activity.description} +

+
+
+ ))} +
+
+
+ + {/* What's All Included banner */} +
+
+

+ All-Inclusive Means All-Inclusive +

+ +
+ {[ + { icon: '🏨', label: 'Luxury Resort Room' }, + { icon: '🍽️', label: 'All Meals Included' }, + { icon: '🍹', label: 'Unlimited Drinks' }, + { icon: '🏊', label: 'Pools & Beach' }, + { icon: '🎭', label: 'Kids Club' }, + { icon: '🎪', label: 'Evening Shows' }, + { icon: '🏄', label: 'Water Sports' }, + { icon: '🎯', label: '18 Months to Book' }, + ].map((item) => ( +
+ {item.icon} +

{item.label}

+
+ ))} +
+
+
+ + {/* Destination Previews */} +
+
+
+

+ Pick Your Family's Paradise +

+

+ Four family-friendly destinations to choose from +

+
+ +
+ {DESTINATIONS.map((dest) => ( +
+
+ {dest.name} +
+
+

+ {dest.name} +

+

+ {dest.tagline} +

+
+
+ ))} +
+
+
+ + {/* Ebook Section */} +
+
+ +

+ Free Family Travel Guide +

+

+ Budget Luxury Travel: 5 secrets to luxury Mexico vacations on a budget +

+

+ Tips on traveling with kids, packing lists, and picking the right resort +

+ + +
+
+ + {/* Pay Now */} +
+
+
+

+ Ready for Family Fun? +

+

+ 5 days, 4 nights, all-inclusive — the whole family +

+
+ +
+
+ + ${PAYMENT_CONFIG.monthlyPrice} + + /mo for {PAYMENT_CONFIG.totalMonths} months +

+ or ${PAYMENT_CONFIG.oneTimePrice} one-time · 30-day money-back guarantee +

+
+ + +
+ + +
+
+ + {/* Family Testimonials */} +
+
+
+

+ Families Love It Here +

+
+ +
+ {familyTestimonials.map((t) => ( + + ))} +
+
+
+ + + + {/* FAQ */} +
+
+

+ Family Travel FAQs +

+ + +
+
+ + {/* Final CTA */} +
+
+ + + +
+

+ They'll Remember This Forever +

+

+ Start planning with our free guide. The best family memories are just ahead. +

+
+ +
+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP21LastChance.tsx b/src/components/lp/pages/LP21LastChance.tsx new file mode 100644 index 0000000..4eb8bad --- /dev/null +++ b/src/components/lp/pages/LP21LastChance.tsx @@ -0,0 +1,465 @@ +'use client' + +import { useState, useEffect } from 'react' +import { + AlertTriangle, + Flame, + Clock, + Check, + MapPin, + Star, + Shield, + Zap, + Gift, + Users, + Eye, + ArrowDown, +} from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import TikTokCarousel from '@/components/lp/shared/TikTokCarousel' +import DestinationCarousel from '@/components/lp/shared/DestinationCarousel' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const RED = '#F44336' +const AMBER = '#FFC107' +const DARK_BG = '#0D0D0D' + +const FEATURES = [ + { + icon: MapPin, + title: '4 Premium Destinations', + description: 'Cancun, Cabo, Riviera Maya, or Puerto Vallarta', + }, + { + icon: Clock, + title: '5 Days / 4 Nights', + description: 'A real vacation — not a weekend trip', + }, + { + icon: Gift, + title: 'All-Inclusive', + description: 'Every meal, every drink, every activity included', + }, + { + icon: Star, + title: '4-5 Star Resorts', + description: 'Luxury properties with world-class amenities', + }, + { + icon: Shield, + title: '30-Day Refund', + description: 'Full money back if you change your mind', + }, + { + icon: Zap, + title: '18 Months to Book', + description: 'Flexible scheduling on your terms', + }, +] + +function PulsingDot({ color = RED }: { color?: string }) { + return ( + + + + + ) +} + +function LiveVisitorCount() { + const [count, setCount] = useState(347) + const [claimed, setClaimed] = useState(23) + + useEffect(() => { + const interval = setInterval(() => { + setCount(prev => prev + (Math.random() > 0.5 ? 1 : -1)) + if (Math.random() > 0.8) { + setClaimed(prev => prev + 1) + } + }, 3000) + return () => clearInterval(interval) + }, []) + + return ( +
+
+ + + {count} people visited this page today + +
+
+ + + {claimed} certificates claimed in the last hour + +
+
+ ) +} + +function SpotsRemaining() { + const [spots, setSpots] = useState(12) + + useEffect(() => { + const interval = setInterval(() => { + setSpots(prev => { + if (prev <= 3) return prev + return Math.random() > 0.85 ? prev - 1 : prev + }) + }, 8000) + return () => clearInterval(interval) + }, []) + + return ( +
+ + + ONLY {spots} CERTIFICATES LEFT AT THIS PRICE + +
+ ) +} + +export default function LP21LastChance() { + const scrollToForm = () => { + document.getElementById('signup-form')?.scrollIntoView({ behavior: 'smooth' }) + } + + return ( +
+ + + {/* ========== HERO ========== */} +
+ {/* Background glow effect */} +
+
+ +
+ {/* Warning badge */} +
+ + + Final warning — Price expires soon + +
+ + {/* Headline */} +

+ This Price{' '} + + Dies + {' '} + When The Timer Hits Zero +

+ + {/* Giant countdown */} +
+ +
+ + {/* Live visitor count */} +
+ +
+ + {/* Spots remaining */} +
+ +
+ + {/* Price display with anchor */} +
+

Regular price: $59/mo

+
+ + ${PAYMENT_CONFIG.monthlyPrice} + + /mo +
+

+ {PAYMENT_CONFIG.totalMonths} months = ${PAYMENT_CONFIG.totalPrice} total | Or ${PAYMENT_CONFIG.oneTimePrice} one-time +

+
+ + {/* CTA 1: Pulsing red button */} + + +

+ 30-day money-back guarantee. No questions asked. +

+ +
+ +
+
+
+ + {/* ========== WHAT YOU LOSE ========== */} +
+
+
+

+ Don't Lose{' '} + Everything You're About to Get +

+

+ When this timer runs out, you lose access to the lowest price we've ever offered + on a 5-day all-inclusive Mexico vacation. +

+
+ + {/* Second countdown */} +
+ +
+ +
+ {FEATURES.map((feature) => ( +
+ +

{feature.title}

+

{feature.description}

+
+ ))} +
+ + {/* CTA 2 */} +
+ +
+
+
+ + {/* ========== TIKTOK SECTION ========== */} +
+ +
+ + {/* ========== TESTIMONIALS ========== */} +
+
+

+ They Almost Missed Out.{' '} + They Didn't. +

+

+ These travelers locked in the same price you see right now. +

+ +
+ {TESTIMONIALS.slice(0, 3).map((t, i) => ( + + ))} +
+
+
+ + {/* ========== THIRD COUNTDOWN STRIP ========== */} +
+
+ +

+ Time remaining at this price: +

+ +
+
+ + {/* ========== DESTINATIONS ========== */} +
+
+

+ Choose Your Paradise +

+ +
+
+ + {/* ========== SIGNUP FORM ========== */} +
+
+ {/* Mini countdown above form */} +
+
+ + + This price is only guaranteed for + + +
+ +

+ Lock In Your Price Now +

+

+ Don't lose the ${PAYMENT_CONFIG.monthlyPrice}/mo price. + It goes back to{' '} + $59/mo when the timer expires. +

+
+ +
+ +
+ + +
+
+ + {/* ========== FAQ ========== */} + + +
+
+

+ Questions? We've Got Answers. +

+ +
+
+ + {/* ========== FINAL CTA ========== */} +
+
+

+ Last Chance. We Mean It. +

+

+ This page will not be available at this price again. Every second you wait + is a second closer to losing ${PAYMENT_CONFIG.monthlyPrice}/mo forever. +

+ + + +
+ +
+ +

+ 5 days / 4 nights all-inclusive Mexico vacation. + ${PAYMENT_CONFIG.monthlyPrice}/mo x {PAYMENT_CONFIG.totalMonths} months or ${PAYMENT_CONFIG.oneTimePrice} one-time. + 30-day money-back guarantee. +

+
+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP22TheProof.tsx b/src/components/lp/pages/LP22TheProof.tsx new file mode 100644 index 0000000..3fa0770 --- /dev/null +++ b/src/components/lp/pages/LP22TheProof.tsx @@ -0,0 +1,370 @@ +'use client' + +import { useState, useEffect } from 'react' +import { + Star, + Check, + Users, + Award, + ThumbsUp, + ArrowRight, + Shield, + Clock, + MapPin, + Heart, + TrendingUp, + BadgeCheck, +} from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import TikTokCarousel from '@/components/lp/shared/TikTokCarousel' +import ComparisonTable from '@/components/lp/shared/ComparisonTable' +import SocialProofTicker from '@/components/lp/shared/SocialProofTicker' +import DestinationCarousel from '@/components/lp/shared/DestinationCarousel' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const GREEN = '#10B981' +const DARK_TEXT = '#1F2937' + +function StatsBar() { + const [claimedCount, setClaimedCount] = useState(2847) + + useEffect(() => { + const interval = setInterval(() => { + if (Math.random() > 0.6) { + setClaimedCount(prev => prev + 1) + } + }, 5000) + return () => clearInterval(interval) + }, []) + + const stats = [ + { value: claimedCount.toLocaleString(), label: 'Certificates Claimed', icon: Users }, + { value: '4.8', label: 'Average Rating', icon: Star, suffix: '/5 ★' }, + { value: '98%', label: 'Would Recommend', icon: ThumbsUp }, + { value: '4+', label: 'Years in Business', icon: Award }, + ] + + return ( +
+
+
+ {stats.map((stat) => ( +
+ +

+ {stat.value} + {stat.suffix && {stat.suffix}} +

+

{stat.label}

+
+ ))} +
+
+
+ ) +} + +function RecentActivity() { + const [activities, setActivities] = useState([ + { name: 'Sarah M.', location: 'TX', action: 'claimed a certificate', time: '2 min ago' }, + { name: 'David K.', location: 'FL', action: 'booked Cancun', time: '5 min ago' }, + { name: 'Jessica R.', location: 'CA', action: 'claimed a certificate', time: '8 min ago' }, + { name: 'Mike T.', location: 'NY', action: 'chose Riviera Maya', time: '12 min ago' }, + { name: 'Amanda L.', location: 'IL', action: 'claimed a certificate', time: '15 min ago' }, + ]) + + return ( +
+ {activities.map((activity, i) => ( +
+ +

+ {activity.name} from {activity.location} {activity.action} +

+ {activity.time} +
+ ))} +
+ ) +} + +function VerifiedBadge() { + return ( + + + Verified + + ) +} + +export default function LP22TheProof() { + const scrollToForm = () => { + document.getElementById('signup-form')?.scrollIntoView({ behavior: 'smooth' }) + } + + return ( +
+ + + {/* ========== HERO ========== */} +
+
+ {/* Social proof badge */} +
+ + + 2,847+ happy travelers and counting + + +
+ +

+ Don't Take Our Word For It — +
+ Watch Real Travelers +

+ +

+ Thousands of people just like you have already locked in their Mexico vacation + at ${PAYMENT_CONFIG.monthlyPrice}/mo. Here's what they're saying. +

+ + {/* Timer strip */} +
+ + + Don't lose the $59/mo{' '} + ${PAYMENT_CONFIG.monthlyPrice}/mo price — expires in + + +
+ + {/* CTA 1 */} +
+ +
+
+
+ + {/* ========== STATS BAR ========== */} + + + {/* ========== TIKTOK HERO SECTION ========== */} +
+ +
+ + {/* ========== TESTIMONIAL WALL ========== */} +
+
+
+

+ Real Reviews From Real Travelers +

+

+ Every single review is from a verified certificate holder +

+
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} + 4.8 out of 5 + (2,847 reviews) +
+
+ + {/* All 6 testimonials */} +
+ {TESTIMONIALS.map((t, i) => ( +
+ +
+ +
+
+ ))} +
+ + {/* CTA 2 */} +
+ +
+
+
+ + {/* ========== RECENT ACTIVITY ========== */} +
+
+
+

+ Happening Right Now +

+

+ Real people claiming their certificates as you read this +

+
+ +

+ + 23 people claimed this in the last hour +

+
+
+ + {/* ========== COUNTDOWN STRIP ========== */} +
+
+

+ Don't lose the ${PAYMENT_CONFIG.monthlyPrice}/mo price — only available for: +

+ +
+
+ + {/* ========== COMPARISON TABLE ========== */} +
+
+
+

+ See How We Compare +

+

+ The same vacation that costs $2,500+ on Expedia — for just ${PAYMENT_CONFIG.oneTimePrice} +

+
+ +
+
+ + {/* ========== DESTINATIONS ========== */} +
+
+

+ 4 Stunning Destinations +

+

+ Choose from Mexico's most beautiful resort locations +

+ +
+
+ + {/* ========== SIGNUP FORM ========== */} +
+
+
+
+ + + Only available for + +
+ +

+ Join 2,847 Happy Travelers +

+

+ Don't lose the{' '} + $59/mo{' '} + ${PAYMENT_CONFIG.monthlyPrice}/mo price. + It won't last. +

+
+ +
+ +
+ + +
+
+ + + + {/* ========== FAQ ========== */} +
+
+

+ Frequently Asked Questions +

+ +
+
+ + {/* ========== FINAL CTA ========== */} +
+
+

+ 2,847 People Can't Be Wrong +

+

+ Don't lose the ${PAYMENT_CONFIG.monthlyPrice}/mo price. Once the timer hits zero, + it goes back to $59/mo. +

+ +
+ +
+ + + +

+ 5 days / 4 nights all-inclusive. ${PAYMENT_CONFIG.monthlyPrice}/mo x{' '} + {PAYMENT_CONFIG.totalMonths} months. 30-day money-back guarantee. +

+
+
+ + + +
+ ) +} diff --git a/src/components/lp/pages/LP23VIPAccess.tsx b/src/components/lp/pages/LP23VIPAccess.tsx new file mode 100644 index 0000000..e672c3a --- /dev/null +++ b/src/components/lp/pages/LP23VIPAccess.tsx @@ -0,0 +1,462 @@ +'use client' + +import { useState, useEffect, useMemo } from 'react' +import { + Crown, + Lock, + Shield, + Star, + Clock, + Check, + ArrowRight, + Gem, + MapPin, + Gift, + Zap, + KeyRound, + Sparkles, +} from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import DestinationCarousel from '@/components/lp/shared/DestinationCarousel' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const BLACK = '#0A0A0F' +const GOLD = '#D4AF37' +const CREAM = '#F5F0E8' +const DARK_SURFACE = '#14141A' + +const VIP_PERKS = [ + { + icon: Crown, + title: 'Priority Resort Selection', + description: 'VIP certificate holders get first pick of available dates and rooms', + }, + { + icon: Gem, + title: 'Premium Room Upgrade', + description: 'Complimentary upgrade to ocean view when available', + }, + { + icon: Gift, + title: 'Welcome Package', + description: 'Exclusive amenities basket delivered to your room on arrival', + }, + { + icon: Star, + title: '4-5 Star All-Inclusive', + description: 'Every meal, drink, and activity included for 5 days', + }, + { + icon: MapPin, + title: '4 Luxury Destinations', + description: 'Cancun, Cabo, Riviera Maya, or Puerto Vallarta', + }, + { + icon: Shield, + title: 'VIP Money-Back Guarantee', + description: '30-day full refund with no questions asked', + }, +] + +function InvitationCode() { + const code = useMemo(() => { + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' + const segments = [] + for (let s = 0; s < 3; s++) { + let segment = '' + for (let i = 0; i < 4; i++) { + segment += chars[Math.floor(Math.random() * chars.length)] + } + segments.push(segment) + } + return segments.join('-') + }, []) + + return ( +
+ +
+

+ Your Invitation Code +

+

+ {code} +

+
+
+ ) +} + +function GoldDivider() { + return ( +
+
+ +
+
+ ) +} + +function ExclusivityCounter() { + const [remaining, setRemaining] = useState(8) + + useEffect(() => { + const interval = setInterval(() => { + setRemaining(prev => { + if (prev <= 2) return prev + return Math.random() > 0.9 ? prev - 1 : prev + }) + }, 12000) + return () => clearInterval(interval) + }, []) + + return ( +
+ + + + + + Only {remaining} VIP certificates remaining at this price + +
+ ) +} + +export default function LP23VIPAccess() { + const scrollToForm = () => { + document.getElementById('signup-form')?.scrollIntoView({ behavior: 'smooth' }) + } + + return ( +
+ + + {/* ========== HERO ========== */} +
+ {/* Gold ambient glow */} +
+ {/* Gold border lines */} +
+ +
+ {/* VIP badge */} +
+ + + Private Invitation + +
+ +

+ You've Been{' '} + + Personally Selected + +
+ For VIP Access +

+ +

+ This private invitation grants you exclusive access to our lowest-ever pricing on a + 5-day, 4-night all-inclusive Mexico vacation at a luxury resort. +

+ + + + {/* Invitation Code */} +
+ +
+ + {/* Timer */} +
+

+ This private link expires in: +

+ +
+ + + + {/* Price */} +
+

+ Standard rate: $59/mo +

+
+ + ${PAYMENT_CONFIG.monthlyPrice} + + /mo +
+

+ {PAYMENT_CONFIG.totalMonths} months = ${PAYMENT_CONFIG.totalPrice} total | Or ${PAYMENT_CONFIG.oneTimePrice} one-time +

+
+ + {/* CTA 1 */} + + +

+ Invitation valid only while the timer is running +

+
+
+ + {/* ========== VIP PERKS ========== */} +
+
+
+

+ Your VIP Certificate Includes +

+

+ Don't lose these exclusive benefits when the timer runs out +

+
+ +
+ {VIP_PERKS.map((perk) => ( +
+ +

+ {perk.title} +

+

+ {perk.description} +

+
+ ))} +
+ + {/* CTA 2 */} +
+ +
+
+
+ + {/* ========== DESTINATIONS ========== */} +
+
+

+ Select Your Destination +

+

+ Four of Mexico's most exclusive resort destinations +

+ +
+
+ + {/* ========== GOLD COUNTDOWN STRIP ========== */} +
+
+ +

+ Your private invitation expires in: +

+ +
+
+ + {/* ========== TESTIMONIALS ========== */} +
+
+

+ What Our VIP Travelers Say +

+ +
+ {TESTIMONIALS.slice(0, 3).map((t, i) => ( + + ))} +
+
+
+ + {/* ========== SIGNUP FORM ========== */} +
+
+
+ +

+ Accept Your VIP Invitation +

+

+ Don't lose the exclusive{' '} + $59/mo{' '} + ${PAYMENT_CONFIG.monthlyPrice}/mo VIP rate. +

+
+ + + Invitation expires in + +
+
+ +
+ +
+ + +
+
+ + {/* ========== FAQ ========== */} + + +
+
+

+ Questions About Your Invitation +

+ +
+
+ + {/* ========== FINAL CTA ========== */} +
+
+
+ +

+ This Invitation Won't Wait +

+

+ Once the timer expires, this VIP pricing is permanently gone. + Don't lose your spot. +

+ + + +
+ +
+ +

+ 5 days / 4 nights all-inclusive at a luxury resort. + ${PAYMENT_CONFIG.monthlyPrice}/mo x {PAYMENT_CONFIG.totalMonths} months or ${PAYMENT_CONFIG.oneTimePrice} one-time. + 30-day money-back guarantee. +

+
+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP24OneTap.tsx b/src/components/lp/pages/LP24OneTap.tsx new file mode 100644 index 0000000..01ca991 --- /dev/null +++ b/src/components/lp/pages/LP24OneTap.tsx @@ -0,0 +1,393 @@ +'use client' + +import { useState, useEffect, useRef } from 'react' +import { + ArrowRight, + Check, + Shield, + Star, + Clock, + Lock, + ChevronDown, + Zap, + MapPin, + BadgeCheck, + Sparkles, + X, +} from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS, DESTINATIONS } from '@/app/lp/_config/types' + +const BLUE = '#2563EB' + +const QUICK_FACTS = [ + { icon: MapPin, text: 'Cancun, Cabo, Riviera Maya, or Puerto Vallarta' }, + { icon: Clock, text: '5 days / 4 nights all-inclusive' }, + { icon: Star, text: '4-5 star luxury resorts' }, + { icon: Shield, text: '30-day money-back guarantee' }, +] + +function SlideUpForm({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) { + const formRef = useRef(null) + + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden' + } else { + document.body.style.overflow = '' + } + return () => { + document.body.style.overflow = '' + } + }, [isOpen]) + + if (!isOpen) return null + + return ( +
+ {/* Backdrop */} +
+ + {/* Slide-up panel */} +
+
+
+

Lock in your price

+

Takes 60 seconds. Seriously.

+
+ +
+ +
+ {/* Timer */} +
+ + + $59/mo{' '} + ${PAYMENT_CONFIG.monthlyPrice}/mo expires in + + +
+ + +
+
+
+ ) +} + +function SpeedBadge() { + return ( +
+ + 60-second signup +
+ ) +} + +function MinimalTestimonial({ quote, name }: { quote: string; name: string }) { + return ( +
+
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+

“{quote}”

+

{name}

+
+ ) +} + +function DestinationPill({ name, image }: { name: string; image: string }) { + return ( +
+ {name} +
+

{name}

+

All-inclusive resort

+
+
+ ) +} + +export default function LP24OneTap() { + const [formOpen, setFormOpen] = useState(false) + const [claimedCount] = useState(() => Math.floor(Math.random() * 15) + 18) + + const scrollToForm = () => { + setFormOpen(true) + } + + return ( +
+ + + {/* ========== HERO — Mobile-first, minimal ========== */} +
+
+ {/* Speed badge */} +
+ +
+ + {/* Beach image */} +
+ Mexico beach paradise +
+
+

$59/mo

+
+ + ${PAYMENT_CONFIG.monthlyPrice} + + /mo +
+

+ 5 days / 4 nights all-inclusive Mexico vacation +

+
+
+ + {/* Timer */} +
+ + + Don't lose this price — expires in + + +
+ + {/* ONE BIG BUTTON — CTA 1 */} + + +

+ Takes 60 seconds. Seriously. +

+ + {/* Scarcity */} +
+

+ + + {claimedCount} people claimed this in the last hour + +

+
+ + {/* Trust badges — compact */} + +
+
+ + {/* ========== QUICK FACTS ========== */} +
+
+

+ What you get +

+
+ {QUICK_FACTS.map((fact) => ( +
+
+ +
+

{fact.text}

+
+ ))} +
+ + {/* CTA 2 */} + +
+
+ + {/* ========== DESTINATIONS ========== */} +
+
+

+ Choose your destination +

+
+ {DESTINATIONS.map((d) => ( + + ))} +
+
+
+ + {/* ========== MINI COUNTDOWN ========== */} +
+
+ + $59/mo ${PAYMENT_CONFIG.monthlyPrice}/mo + price expires in + + +
+
+ + {/* ========== TESTIMONIALS ========== */} +
+
+

+ Real travelers, real reviews +

+
+ {TESTIMONIALS.slice(0, 3).map((t, i) => ( + + ))} +
+ +
+

+ + 4.8/5 from 2,847 reviews +

+
+
+
+ + {/* ========== PRICE BREAKDOWN ========== */} +
+
+

+ Two ways to pay +

+ +
+ {/* Monthly */} +
+

Most popular

+

$59/mo

+

+ ${PAYMENT_CONFIG.monthlyPrice}/mo +

+

x {PAYMENT_CONFIG.totalMonths} months

+
+ + {/* One-time */} +
+

Save more

+

$590

+

+ ${PAYMENT_CONFIG.oneTimePrice} +

+

one payment

+
+
+ + {/* CTA 3 */} + + +
+ SSL Encrypted + 30-Day Guarantee +
+
+
+ + + + {/* ========== FAQ ========== */} +
+
+

+ Quick answers +

+ +
+
+ + {/* ========== FINAL CTA ========== */} +
+
+

+ Don't lose this price +

+

+ ${PAYMENT_CONFIG.monthlyPrice}/mo for a 5-day all-inclusive Mexico vacation. + This price won't last. +

+ +
+ +
+ + + +

+ Takes 60 seconds. 30-day money-back guarantee. +

+
+
+ + {/* Slide-up form panel */} + setFormOpen(false)} /> + + +
+ ) +} diff --git a/src/components/lp/pages/LP25FOMOFeed.tsx b/src/components/lp/pages/LP25FOMOFeed.tsx new file mode 100644 index 0000000..f2ab83c --- /dev/null +++ b/src/components/lp/pages/LP25FOMOFeed.tsx @@ -0,0 +1,504 @@ +'use client' + +import { useState, useEffect } from 'react' +import { + ArrowRight, + Clock, + Users, + Star, + Check, + Flame, + Heart, + MapPin, + Shield, + Zap, + TrendingUp, + Eye, + Sparkles, +} from 'lucide-react' +import { motion, AnimatePresence } from 'framer-motion' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import TikTokCarousel from '@/components/lp/shared/TikTokCarousel' +import SocialProofTicker from '@/components/lp/shared/SocialProofTicker' +import DestinationCarousel from '@/components/lp/shared/DestinationCarousel' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS, DESTINATIONS } from '@/app/lp/_config/types' + +const GRADIENT_FROM = '#EC4899' // pink-500 +const GRADIENT_VIA = '#F97316' // orange-500 +const GRADIENT_TO = '#EAB308' // yellow-500 + +const FEED_NAMES = [ + 'Maria T. from Houston, TX', + 'James & Linda from Miami, FL', + 'Sarah K. from San Diego, CA', + 'David M. from Brooklyn, NY', + 'Jennifer W. from Chicago, IL', + 'Chris P. from Phoenix, AZ', + 'Amanda R. from Denver, CO', + 'Robert J. from Atlanta, GA', + 'Nicole S. from Seattle, WA', + 'Brian L. from Dallas, TX', + 'Emily H. from Nashville, TN', + 'Kevin D. from Boston, MA', + 'Rachel G. from Portland, OR', + 'Mike & Julie from Austin, TX', + 'Lisa F. from Charlotte, NC', +] + +const FEED_ACTIONS = [ + 'just claimed their vacation certificate', + 'locked in the $29/mo price', + 'is heading to Cancun!', + 'booked Cabo San Lucas', + 'chose Riviera Maya', + 'signed up 2 minutes ago', + 'just started their payment plan', + 'is going to Puerto Vallarta!', +] + +function LiveActivityFeed() { + const [entries, setEntries] = useState>([]) + const [nextId, setNextId] = useState(0) + + useEffect(() => { + // Seed initial entries + const initial = Array.from({ length: 5 }, (_, i) => ({ + name: FEED_NAMES[i % FEED_NAMES.length], + action: FEED_ACTIONS[i % FEED_ACTIONS.length], + id: i, + })) + setEntries(initial) + setNextId(5) + + const interval = setInterval(() => { + setNextId(prev => { + const newId = prev + 1 + const newEntry = { + name: FEED_NAMES[newId % FEED_NAMES.length], + action: FEED_ACTIONS[newId % FEED_ACTIONS.length], + id: newId, + } + setEntries(prevEntries => [newEntry, ...prevEntries.slice(0, 6)]) + return newId + }) + }, 4000) + + return () => clearInterval(interval) + }, []) + + return ( +
+ + {entries.map((entry) => ( + + +
+

+ {entry.name} +

+

{entry.action}

+
+ just now +
+ ))} +
+ {/* Fade overlay at bottom */} +
+
+ ) +} + +function GradientButton({ onClick, children, className = '' }: { onClick: () => void; children: React.ReactNode; className?: string }) { + return ( + + ) +} + +function FOMOCounter() { + const [viewers, setViewers] = useState(89) + const [claimed, setClaimed] = useState(34) + + useEffect(() => { + const interval = setInterval(() => { + setViewers(prev => prev + (Math.random() > 0.5 ? 1 : Math.random() > 0.3 ? 0 : -1)) + if (Math.random() > 0.7) setClaimed(prev => prev + 1) + }, 3000) + return () => clearInterval(interval) + }, []) + + return ( +
+
+ + + {viewers} viewing now + +
+
+ + + {claimed} claimed today + +
+
+ + + Only 9 left at this price + +
+
+ ) +} + +function DestinationCard({ name, image, tagline }: { name: string; image: string; tagline: string }) { + return ( +
+ {name} +
+
+

{name}

+

{tagline}

+
+ + {Math.floor(Math.random() * 500) + 800} saves +
+
+
+ ) +} + +export default function LP25FOMOFeed() { + const scrollToForm = () => { + document.getElementById('signup-form')?.scrollIntoView({ behavior: 'smooth' }) + } + + return ( +
+ + + {/* ========== HERO ========== */} +
+ {/* Gradient background */} +
+ +
+ {/* Trending badge */} +
+ + Trending — 2,847 certificates claimed this month +
+ +

+ Everyone's Going to Mexico. +
+ + Here's Why. + +

+ +

+ 5 days, 4 nights, all-inclusive at a luxury resort — for just{' '} + $59/mo{' '} + ${PAYMENT_CONFIG.monthlyPrice}/mo. + Don't be the last to know. +

+ + {/* Timer */} +
+ + + Don't lose this price — expires in + + +
+ + {/* FOMO counters */} +
+ +
+ + {/* CTA 1 */} + + Don't Miss Out — Claim Yours Now + + + +

+ 30-day money-back guarantee. Cancel anytime. +

+
+
+ + {/* ========== TIKTOK SECTION ========== */} +
+ +
+ + {/* ========== LIVE ACTIVITY FEED ========== */} +
+
+
+
+ + LIVE +
+

+ Happening Right Now +

+

+ Watch as people claim their certificates in real time +

+
+ + + +
+ + Don't Be the Only One Missing Out + + +
+
+
+ + {/* ========== GRADIENT COUNTDOWN STRIP ========== */} +
+
+

+ $59/mo ${PAYMENT_CONFIG.monthlyPrice}/mo pricing ends in: +

+ +
+
+ + {/* ========== DESTINATIONS ========== */} +
+
+
+

+ Where Will You Go? +

+

Four stunning destinations. One unbeatable price.

+
+
+ {DESTINATIONS.map((d) => ( + + ))} +
+
+
+ + {/* ========== TESTIMONIALS ========== */} +
+
+
+

+ They Went. They Loved It. +

+

+ Real reviews from travelers who didn't miss out +

+
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} + 4.8/5 (2,847 reviews) +
+
+ +
+ {TESTIMONIALS.slice(0, 6).map((t, i) => ( + + ))} +
+ + {/* CTA 3 */} +
+ + Join 2,847 Happy Travelers + + +
+
+
+ + {/* ========== WHAT'S INCLUDED ========== */} +
+
+

+ Everything's Included +

+
+ {[ + '5 days / 4 nights accommodation', + 'All meals — breakfast, lunch, dinner', + 'Unlimited drinks (alcoholic & non)', + 'Resort pools, beach, & amenities', + '4-5 star luxury resort', + 'Your choice of 4 destinations', + 'Flexible dates within 18 months', + '30-day money-back guarantee', + ].map((item) => ( +
+
+ +
+ {item} +
+ ))} +
+
+
+ + {/* ========== SIGNUP FORM ========== */} +
+
+
+
+ + Don't miss out +
+ +

+ Everyone Else Is Going. +
+ + Are You? + +

+ +

+ $59/mo{' '} + ${PAYMENT_CONFIG.monthlyPrice}/mo{' '} + — only available for{' '} + +

+
+ +
+ +
+ + +
+
+ + + + {/* ========== FAQ ========== */} +
+
+

+ Got Questions? +

+ +
+
+ + {/* ========== FINAL CTA ========== */} +
+
+

+ Don't Be the Only One Missing Out +

+

+ 2,847 people have already claimed their Mexico vacation certificate. + The ${PAYMENT_CONFIG.monthlyPrice}/mo price disappears when the timer hits zero. +

+ +
+ +
+ + + +

+ 5 days / 4 nights all-inclusive. ${PAYMENT_CONFIG.monthlyPrice}/mo x{' '} + {PAYMENT_CONFIG.totalMonths} months or ${PAYMENT_CONFIG.oneTimePrice} one-time. + 30-day money-back guarantee. +

+
+
+ + + +
+ ) +} diff --git a/src/components/lp/pages/LP26PriceLock.tsx b/src/components/lp/pages/LP26PriceLock.tsx new file mode 100644 index 0000000..cca1e09 --- /dev/null +++ b/src/components/lp/pages/LP26PriceLock.tsx @@ -0,0 +1,500 @@ +'use client' + +import { useState, useEffect } from 'react' +import { + Lock, Shield, ShieldCheck, Clock, CheckCircle2, + ArrowRight, Zap, Users, Star, KeyRound, + LockKeyhole, BadgeCheck, Gift, TrendingUp, +} from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import TikTokCarousel from '@/components/lp/shared/TikTokCarousel' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const NAVY = '#1A237E' +const GOLD = '#FFD700' +const DARK_NAVY = '#0D1347' + +const steps = [ + { + icon: LockKeyhole, + title: 'Lock Your Price', + description: 'Secure today\'s $29/mo rate before the timer expires. Your price is frozen the moment you sign up.', + }, + { + icon: KeyRound, + title: 'Choose Your Dates', + description: 'Pick any available dates within 18 months. Cancun, Cabo, Riviera Maya, or Puerto Vallarta.', + }, + { + icon: Gift, + title: 'Enjoy Paradise', + description: '5 days and 4 nights all-inclusive. Meals, drinks, resort amenities — everything covered.', + }, +] + +const securityFeatures = [ + { icon: Shield, text: '256-bit SSL encryption on all transactions' }, + { icon: ShieldCheck, text: '30-day money-back guarantee — no questions asked' }, + { icon: BadgeCheck, text: 'BBB-accredited business with 4.8-star rating' }, + { icon: Lock, text: 'Price locked for 10 months — zero increases, ever' }, +] + +export default function LP26PriceLock() { + const [claimedPercent, setClaimedPercent] = useState(84) + const [viewerCount] = useState(() => Math.floor(Math.random() * 18) + 12) + + useEffect(() => { + const interval = setInterval(() => { + setClaimedPercent(prev => { + if (prev >= 96) return prev + return prev + (Math.random() > 0.6 ? 1 : 0) + }) + }, 8000) + return () => clearInterval(interval) + }, []) + + const scrollToForm = () => { + document.getElementById('signup-form')?.scrollIntoView({ behavior: 'smooth' }) + } + + return ( +
+ + + + {/* ===== HERO ===== */} +
+ {/* Vault pattern overlay */} +
+
+
+ +
+ {/* Viewers badge */} +
+ + + {viewerCount} people viewing this offer right now + +
+ + {/* Vault lock icon */} +
+
+ +
+
+ $29 +
+
+ +

+ PRICE LOCK{' '} + GUARANTEE +

+ +

+ $29/mo locked for the next{' '} + 30 minutes +

+ +

+ 5 days & 4 nights all-inclusive Mexico vacation.{' '} + $59/mo{' '} + $29/mo — save $200 +

+ + {/* Countdown block */} +
+

+ After the timer expires, price increases to $59/mo +

+ +
+ +
+ +
+ + +
+
+ + {/* ===== PROGRESS BAR — CERTIFICATES CLAIMED ===== */} +
+
+
+ + + Certificates Claimed at This Price + + + {claimedPercent}% claimed + +
+
+
+
+
+
+

+ Only {100 - claimedPercent}% of discounted certificates remain — don't lose yours +

+
+
+ + {/* ===== LOSS AVERSION SECTION ===== */} +
+
+

+ Don't Lose This Price +

+

+ Every minute that passes, someone else claims a certificate at this rate. + Once they're gone, the price goes back to{' '} + $59/mo. +

+ +
+ {/* What you lose */} +
+

+ + If You Wait... +

+
    + {[ + 'Price jumps to $59/mo ($200 more total)', + 'Your spot may be taken by someone else', + 'No guarantee this promotion will return', + 'You\'ll regret not acting when you had the chance', + ].map((item, i) => ( +
  • + + {item} +
  • + ))} +
+
+ + {/* What you keep */} +
+

+ + If You Lock In Now... +

+
    + {[ + '$29/mo locked — price NEVER increases', + '5 days/4 nights all-inclusive Mexico vacation', + 'Choose from 4 stunning destinations', + '30-day money-back guarantee if you change your mind', + ].map((item, i) => ( +
  • + + {item} +
  • + ))} +
+
+
+ + +
+
+ + {/* ===== HOW IT WORKS — 3 STEPS ===== */} +
+
+

+ How It Works +

+

+ Three simple steps to your locked-in paradise vacation +

+ +
+ {steps.map((step, i) => ( +
+
+ {i + 1} +
+
+ +
+

+ {step.title} +

+

{step.description}

+
+ ))} +
+
+
+ + {/* ===== SECURITY FEATURES ===== */} +
+
+

+ Your Investment Is Protected +

+

+ We take your security as seriously as we take your vacation experience +

+ +
+ {securityFeatures.map((feat, i) => ( +
+
+ +
+

{feat.text}

+
+ ))} +
+
+
+ + {/* ===== TIKTOK CAROUSEL ===== */} + + + {/* ===== COUNTDOWN REMINDER ===== */} +
+
+

+ Your Price Lock Expires In +

+ +

+ After the timer runs out, the price increases to{' '} + $59/mo.{' '} + Don't lose your spot. +

+
+
+ + {/* ===== TESTIMONIALS ===== */} +
+
+

+ Travelers Who Locked In Their Price +

+
+ {TESTIMONIALS.slice(0, 3).map((t, i) => ( + + ))} +
+
+
+ + {/* ===== PRICING ANCHOR ===== */} +
+
+

+ The Numbers Don't Lie +

+

Similar vacations cost $3,000+. You pay a fraction.

+ +
+
+ $59/mo + regular price +
+
+ + ${PAYMENT_CONFIG.monthlyPrice} + + /mo +
+

+ {PAYMENT_CONFIG.totalMonths} months × ${PAYMENT_CONFIG.monthlyPrice} = ${PAYMENT_CONFIG.totalPrice} +

+

+ or ${PAYMENT_CONFIG.oneTimePrice} one-time payment +

+ +
+ + You save $200 vs. regular price +
+ + +
+
+
+ + {/* ===== FORM ===== */} +
+
+
+ +

+ Lock In Your Price Now +

+

+ $59/mo{' '} + ${PAYMENT_CONFIG.monthlyPrice}/mo{' '} + — 5 days/4 nights all-inclusive +

+
+ + + +
+ +
+
+
+ + {/* ===== FAQ ===== */} + + +
+
+

+ Common Questions +

+ +
+
+ + {/* ===== FINAL CTA ===== */} +
+
+ +

+ Time Is Running Out — Lock In $29/mo Before It's Gone +

+ +
+
+
+ ) +} diff --git a/src/components/lp/pages/LP27BeforeAfter.tsx b/src/components/lp/pages/LP27BeforeAfter.tsx new file mode 100644 index 0000000..4e94776 --- /dev/null +++ b/src/components/lp/pages/LP27BeforeAfter.tsx @@ -0,0 +1,443 @@ +'use client' + +import { useState, useEffect } from 'react' +import { + Clock, Coffee, Monitor, Car, Frown, CloudRain, + Sun, Waves, UtensilsCrossed, Palmtree, Smile, Music, + ArrowRight, ArrowDown, CheckCircle2, Star, Heart, + Sparkles, Camera, MapPin, +} from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import DestinationCarousel from '@/components/lp/shared/DestinationCarousel' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const GRAY = '#6B7280' +const TEAL = '#0D9488' +const DARK_TEAL = '#065F53' +const LIGHT_TEAL = '#F0FDFA' + +const beforeStats = [ + { icon: Clock, stat: '8.5 hrs', label: 'Average workday' }, + { icon: Car, stat: '52 min', label: 'Daily commute' }, + { icon: Monitor, stat: '11 hrs', label: 'Screen time' }, + { icon: Coffee, stat: '3.5 cups', label: 'Coffee to survive' }, +] + +const afterStats = [ + { icon: Sun, stat: '0', label: 'Alarms set' }, + { icon: Waves, stat: '∞', label: 'Beach hours' }, + { icon: UtensilsCrossed, stat: 'All', label: 'Meals included' }, + { icon: Smile, stat: '100%', label: 'Relaxation' }, +] + +const beforeItems = [ + { icon: Frown, text: 'Staring at the same four walls every day' }, + { icon: CloudRain, text: 'Weather that matches your mood — gray' }, + { icon: Monitor, text: 'Endless emails, meetings, and deadlines' }, + { icon: Car, text: 'Sitting in traffic, wasting your life' }, + { icon: Coffee, text: 'Running on caffeine and fumes' }, + { icon: Clock, text: 'Counting down to Friday... again' }, +] + +const afterItems = [ + { icon: Palmtree, text: 'Waking up to ocean views and warm breeze' }, + { icon: Sun, text: 'Sun on your skin, sand between your toes' }, + { icon: UtensilsCrossed, text: 'World-class cuisine — every meal, every day' }, + { icon: Music, text: 'Live music, cocktails, and sunset magic' }, + { icon: Heart, text: 'Quality time with the person who matters most' }, + { icon: Camera, text: 'Photos that make everyone jealous' }, +] + +const transformations = [ + { + before: 'Fluorescent office lights', + after: 'Golden hour sunsets over the Pacific', + image: '/images/cdn/photo-1507525428034-b723cf961d3e.jpg', + }, + { + before: 'Sad desk lunch', + after: 'Oceanfront dining with unlimited cocktails', + image: '/images/cdn/photo-1544551763-46a013bb70d5.jpg', + }, + { + before: 'Scrolling through vacation photos', + after: 'Being IN the vacation photos', + image: '/images/cdn/photo-1510097467424-192d713fd8b2.jpg', + }, +] + +export default function LP27BeforeAfter() { + const [activeTransform, setActiveTransform] = useState(0) + + useEffect(() => { + const interval = setInterval(() => { + setActiveTransform(prev => (prev + 1) % transformations.length) + }, 5000) + return () => clearInterval(interval) + }, []) + + const scrollToForm = () => { + document.getElementById('signup-form')?.scrollIntoView({ behavior: 'smooth' }) + } + + return ( +
+ + + + {/* ===== HERO — SPLIT SCREEN ===== */} +
+
+ {/* BEFORE — Gray, desaturated */} +
+
+
+

+ Your Monday +

+

+ Same Desk.{' '} + Same Grind. +

+

+ Another day, another dollar. Another year, no vacation. + You're reading this because you know you deserve better. +

+ +
+
+ + {/* AFTER — Vibrant, full color */} +
+
+
+
+

+ Your Vacation +

+

+ Ocean Views.{' '} + Pure Bliss. +

+

+ 5 days and 4 nights of all-inclusive paradise. Meals, drinks, sunshine — all yours. +

+ +
+
+
+ + {/* Center divider with CTA */} +
+

+ You deserve more than a{' '} + screensaver of a beach +

+

+ $59/mo{' '} + ${PAYMENT_CONFIG.monthlyPrice}/mo{' '} + for the real thing +

+ +
+
+ + {/* ===== BEFORE STATS VS AFTER STATS ===== */} +
+
+

+ Your Life By the Numbers +

+ +
+ {/* Before stats */} +
+

+ Your Average Week +

+
+ {beforeStats.map((s, i) => ( +
+ +

{s.stat}

+

{s.label}

+
+ ))} +
+
+ + {/* After stats */} +
+

+ Your Vacation Week +

+
+ {afterStats.map((s, i) => ( +
+ +

{s.stat}

+

{s.label}

+
+ ))} +
+
+
+
+
+ + {/* ===== BEFORE / AFTER LIST COMPARISON ===== */} +
+
+
+ {/* Before column */} +
+

+ + Right Now +

+
+ {beforeItems.map((item, i) => ( +
+ + {item.text} +
+ ))} +
+
+ + {/* After column */} +
+

+ + On Vacation +

+
+ {afterItems.map((item, i) => ( +
+ + {item.text} +
+ ))} +
+
+
+ +
+ +
+
+
+ + {/* ===== COUNTDOWN DIVIDER ===== */} +
+
+

+ This pricing disappears in +

+ +

+ $59/mo{' '} + ${PAYMENT_CONFIG.monthlyPrice}/mo — only while the timer lasts +

+
+
+ + {/* ===== TRANSFORMATION SHOWCASE ===== */} +
+
+

+ The Transformation Is{' '} + Real +

+ +
+ {transformations.map((t, i) => ( +
+
+

Before

+

{t.before}

+ +
+
+ {t.after} +
+
+

After

+

{t.after}

+
+
+
+
+ ))} +
+
+
+ + {/* ===== TESTIMONIALS ===== */} +
+
+

+ They Made the Switch +

+

+ Real people who stopped scrolling and started living +

+
+ {TESTIMONIALS.slice(0, 3).map((t, i) => ( + + ))} +
+
+
+ + {/* ===== DESTINATIONS ===== */} +
+
+

+ Choose Your "After" +

+

+ 4 stunning destinations — all included in your certificate +

+ +
+
+ + {/* ===== FORM ===== */} +
+
+
+ +

+ Stop Dreaming. Start Living. +

+

+ $59/mo{' '} + ${PAYMENT_CONFIG.monthlyPrice}/mo{' '} + — 5 days/4 nights all-inclusive +

+
+ + + +
+ +
+
+
+ + {/* ===== FAQ ===== */} + + +
+
+

+ Questions? We've Got Answers +

+ +
+
+ + {/* ===== FINAL CTA ===== */} +
+
+ +

+ Your "Before" Story Ends Today +

+

+ Don't lose this price. Don't lose this moment. +

+ +
+
+
+ ) +} diff --git a/src/components/lp/pages/LP28RiskFree.tsx b/src/components/lp/pages/LP28RiskFree.tsx new file mode 100644 index 0000000..803e21a --- /dev/null +++ b/src/components/lp/pages/LP28RiskFree.tsx @@ -0,0 +1,438 @@ +'use client' + +import { useState } from 'react' +import { + Shield, ShieldCheck, ShieldQuestion, CheckCircle2, + ArrowRight, Calendar, CreditCard, HelpCircle, + ThumbsUp, MessageCircle, Star, BadgeCheck, + RefreshCw, Lock, Heart, Users, + ChevronDown, ChevronUp, Clock, +} from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import ComparisonTable from '@/components/lp/shared/ComparisonTable' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const GREEN = '#059669' +const DARK_GREEN = '#047857' +const LIGHT_GREEN = '#ECFDF5' +const LIGHTER_GREEN = '#F0FDF4' + +const objections = [ + { + icon: HelpCircle, + question: 'What if I don\'t like it?', + answer: 'Full refund within 30 days — no questions asked. If the vacation doesn\'t meet your expectations, we give you every penny back. We\'ve been doing this for years, and our refund rate is less than 2%.', + badge: '30-Day Money-Back', + }, + { + icon: Calendar, + question: 'What if the dates don\'t work for me?', + answer: 'You have 18 months of flexible scheduling. Choose any available dates that work for your calendar. Need to reschedule? No problem — we make it easy.', + badge: 'Flexible Scheduling', + }, + { + icon: ShieldQuestion, + question: 'Is this actually legit?', + answer: 'We\'re a verified, BBB-accredited business with thousands of happy travelers. Real reviews, real people, real vacations. We\'ve been featured on TikTok with millions of views.', + badge: 'BBB Accredited', + }, + { + icon: CreditCard, + question: 'Is my payment information safe?', + answer: '256-bit SSL encryption protects every transaction. We use the same security technology as major banks. Your card details are never stored on our servers.', + badge: 'Bank-Level Security', + }, + { + icon: RefreshCw, + question: 'What if I need to cancel?', + answer: 'Cancel anytime within 30 days for a full refund. After 30 days, your certificate is still transferable — give it to a friend or family member.', + badge: 'Easy Cancellation', + }, + { + icon: Users, + question: 'Can I really bring a guest for free?', + answer: 'Yes! Your certificate covers a family of four — 2 adults and 2 kids under 12. The all-inclusive package — meals, drinks, resort amenities — applies to everyone. Additional guests can be added at a discounted rate.', + badge: 'Bring a Guest Free', + }, +] + +const skepticTestimonials = [ + { + quote: "I was SO skeptical — I almost didn't sign up. Best decision I ever made. The resort was incredible and everything was exactly as described.", + name: "Jennifer & Tom", + location: "New York, NY", + photo: "/images/cdn/photo-1494790108377-be9c29b29330.jpg", + }, + { + quote: "My husband thought it was a scam. We booked anyway. Now he's the one telling everyone about it. Cabo was AMAZING.", + name: "Maria G.", + location: "Houston, TX", + photo: "/images/cdn/photo-1438761681033-6461ffad8d80.jpg", + }, + { + quote: "I did so much research before signing up. Read every review. Turns out it's 100% real — and the vacation exceeded every expectation.", + name: "David & Lisa", + location: "Denver, CO", + photo: "/images/cdn/photo-1472099645785-5658abf4ff4e.jpg", + }, +] + +const trustPoints = [ + { value: '12,000+', label: 'Happy Travelers' }, + { value: '4.8/5', label: 'Average Rating' }, + { value: '<2%', label: 'Refund Rate' }, + { value: '30 Days', label: 'Money-Back Guarantee' }, +] + +export default function LP28RiskFree() { + const [expandedObjection, setExpandedObjection] = useState(0) + + const scrollToForm = () => { + document.getElementById('signup-form')?.scrollIntoView({ behavior: 'smooth' }) + } + + return ( +
+ + + + {/* ===== HERO ===== */} +
+
+ {/* Giant shield */} +
+
+ +
+
+ +
+
+ +

+ 100% RISK-FREE +

+

+ 30-Day Money-Back Guarantee on your 5-day/4-night all-inclusive Mexico vacation +

+

+ $59/mo{' '} + ${PAYMENT_CONFIG.monthlyPrice}/mo{' '} + — if you don't love it, you don't pay +

+ + + + {/* Trust stats row */} +
+ {trustPoints.map((tp, i) => ( +
+

{tp.value}

+

{tp.label}

+
+ ))} +
+
+
+ + {/* ===== RISK-FREE COUNTDOWN ===== */} +
+
+

+ Risk-free offer available for +

+ +

+ After the timer, the price increases to $59/mo +

+
+
+ + {/* ===== OBJECTION HANDLING ===== */} +
+
+

+ Every Worry, Addressed +

+

+ We know you have questions. Here are honest answers to every one. +

+ +
+ {objections.map((obj, i) => ( +
+ + + {expandedObjection === i && ( +
+
+

{obj.answer}

+
+ + + Your concern is fully covered + +
+
+
+ )} +
+ ))} +
+ +
+ +
+
+
+ + {/* ===== GUARANTEE BANNER ===== */} +
+
+
+
+
+
+ +
+
+
+

+ Our Iron-Clad 30-Day Guarantee +

+

+ Book your vacation certificate today. If for ANY reason you're not completely satisfied within 30 days, + contact us and we'll refund every penny. No hoops. No hassle. No hard feelings. + We can offer this because{' '} + 98% of our travelers are thrilled with their experience. +

+
+
+
+
+
+ + {/* ===== COMPARISON TABLE ===== */} +
+
+

+ See How We Compare +

+

+ Same vacation, fraction of the price — plus protections others don't offer +

+ +
+
+ + {/* ===== SKEPTIC TESTIMONIALS ===== */} +
+
+

+ "I Almost Didn't Sign Up..." +

+

+ Hear from travelers who were skeptical — and pleasantly surprised +

+ +
+ {skepticTestimonials.map((t, i) => ( +
+
+ + Former Skeptic + +
+ +
+ ))} +
+
+
+ + {/* ===== WHAT'S INCLUDED ===== */} +
+
+

+ Everything Included, Zero Risk +

+ +
+ {[ + '5 days / 4 nights accommodation', + 'All meals — breakfast, lunch, dinner', + 'Unlimited drinks (alcoholic & non)', + 'Resort pools, beaches, amenities', + 'Choose from 4 Mexico destinations', + '18 months of flexible scheduling', + 'Bring a guest at no extra charge', + '30-day full money-back guarantee', + ].map((item, i) => ( +
+ + {item} +
+ ))} +
+ +
+

+ $59/mo{' '} + ${PAYMENT_CONFIG.monthlyPrice}/mo{' '} + or ${PAYMENT_CONFIG.oneTimePrice} one-time +

+
+
+
+ + {/* ===== FORM ===== */} +
+
+
+ +

+ Try It Completely Risk-Free +

+

+ $59/mo{' '} + ${PAYMENT_CONFIG.monthlyPrice}/mo{' '} + — 30-day money-back guarantee +

+
+ + + +
+

+ + 256-bit SSL encryption • 30-day guarantee • Cancel anytime +

+
+ +
+ +
+
+
+ + + + {/* ===== FAQ ===== */} +
+
+

+ Still Have Questions? +

+ +
+
+ + {/* ===== FINAL CTA ===== */} +
+
+ +

+ Zero Risk. Full Refund If You're Not Amazed. +

+

+ Don't lose this price — the guarantee won't last forever +

+ +
+
+
+ ) +} diff --git a/src/components/lp/pages/LP29SpeedDeal.tsx b/src/components/lp/pages/LP29SpeedDeal.tsx new file mode 100644 index 0000000..04b0ea6 --- /dev/null +++ b/src/components/lp/pages/LP29SpeedDeal.tsx @@ -0,0 +1,408 @@ +'use client' + +import { useState, useEffect } from 'react' +import { + Zap, ArrowRight, Clock, Shield, Lock, + CheckCircle2, Star, Users, TrendingUp, + AlertTriangle, ChevronRight, Flame, +} from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG } from '@/app/lp/_config/types' + +const YELLOW = '#FFC107' +const RED = '#FF1744' +const BLACK = '#000000' +const DARK_GRAY = '#111111' + +export default function LP29SpeedDeal() { + const [claimedCount, setClaimedCount] = useState(47) + const [showPulse, setShowPulse] = useState(true) + const [recentBuyer, setRecentBuyer] = useState('') + const [showBuyer, setShowBuyer] = useState(false) + + const recentBuyers = [ + 'Maria from TX', 'James from FL', 'Sarah from CA', + 'David from NY', 'Jennifer from IL', 'Robert from AZ', + 'Lisa from CO', 'Michael from GA', 'Rachel from PA', + ] + + useEffect(() => { + // Increment claimed count + const claimInterval = setInterval(() => { + setClaimedCount(prev => { + if (prev >= 63) return prev + return prev + (Math.random() > 0.5 ? 1 : 0) + }) + }, 12000) + + // Show recent buyers + let buyerIdx = 0 + const buyerInterval = setInterval(() => { + setRecentBuyer(recentBuyers[buyerIdx % recentBuyers.length]) + setShowBuyer(true) + buyerIdx++ + setTimeout(() => setShowBuyer(false), 3500) + }, 7000) + + // Pulse animation + const pulseInterval = setInterval(() => { + setShowPulse(false) + setTimeout(() => setShowPulse(true), 200) + }, 3000) + + return () => { + clearInterval(claimInterval) + clearInterval(buyerInterval) + clearInterval(pulseInterval) + } + }, []) + + const scrollToForm = () => { + document.getElementById('signup-form')?.scrollIntoView({ behavior: 'smooth' }) + } + + return ( +
+ + + + {/* ===== RECENT BUYER NOTIFICATION ===== */} +
+
+
+ +
+
+

{recentBuyer}

+

just grabbed this deal

+
+
+
+ + {/* ===== HERO ===== */} +
+ {/* Lightning bolt background pattern */} +
+ {[...Array(12)].map((_, i) => ( + + ))} +
+ +
+ {/* Flash deal badge */} +
+ + FLASH DEAL — LIMITED TIME + +
+ + {/* Lightning icon */} +
+ +
+ +

+ FLASH DEAL +

+ +

+ This deal self-destructs in +

+ + {/* Giant countdown */} +
+ +
+ + {/* Price display */} +
+
+ + $59/mo + + + -34% + +
+
+ + ${PAYMENT_CONFIG.monthlyPrice} + + /mo +
+

+ {PAYMENT_CONFIG.totalMonths} months = ${PAYMENT_CONFIG.totalPrice} total{' '} + |{' '} + or ${PAYMENT_CONFIG.oneTimePrice} one-time +

+
+ + {/* CTA Button */} + + + {/* Claimed counter */} +
+ + + {claimedCount} people grabbed this deal in the last hour + +
+
+
+ + {/* ===== WHAT YOU GET — MINIMAL ===== */} +
+
+
+ {[ + '5 days / 4 nights all-inclusive', + 'Cancun, Cabo, Riviera Maya, or PV', + 'All meals + unlimited drinks', + 'Resort pools, beaches, amenities', + 'Bring a guest — included free', + '18 months to choose your dates', + ].map((item, i) => ( +
+ + {item} +
+ ))} +
+
+
+ + {/* ===== URGENCY STRIP ===== */} +
+
+ +

+ When the timer hits zero, this deal is gone forever +

+ +
+
+ + {/* ===== SECOND COUNTDOWN + PRICE ===== */} +
+
+

+ Time Is Running Out +

+ + +
+
+

34%

+

Savings

+
+
+

$200

+

You Save

+
+
+

+ {100 - claimedCount} +

+

Deals Left

+
+
+ + +
+
+ + {/* ===== TRUST — MINIMAL ===== */} +
+
+
+ {[ + { icon: Shield, label: '30-Day Guarantee' }, + { icon: Lock, label: 'SSL Encrypted' }, + { icon: Star, label: '4.8/5 Rating' }, + { icon: Users, label: '12,000+ Travelers' }, + ].map((item, i) => ( +
+ +

{item.label}

+
+ ))} +
+
+
+ + {/* ===== FORM ===== */} +
+ {/* Animated border */} +
+ + + +
+
+ +

+ GRAB THIS DEAL +

+
+ $59/mo + + + ${PAYMENT_CONFIG.monthlyPrice}/mo + +
+

+ 5 days/4 nights all-inclusive Mexico vacation +

+
+ + + +
+

+ + Secure checkout • 30-day money-back guarantee +

+
+
+
+ + + + {/* ===== FINAL COUNTDOWN ===== */} +
+
+

+ Last Chance — Deal Expires When Timer Hits Zero +

+ + +
+
+
+ ) +} diff --git a/src/components/lp/pages/LP30Influencer.tsx b/src/components/lp/pages/LP30Influencer.tsx new file mode 100644 index 0000000..249855c --- /dev/null +++ b/src/components/lp/pages/LP30Influencer.tsx @@ -0,0 +1,538 @@ +'use client' + +import { useState, useEffect } from 'react' +import { + Heart, MessageCircle, Share2, Bookmark, Play, + Eye, Users, TrendingUp, Star, ArrowRight, + CheckCircle2, Shield, Lock, Sparkles, + Music, Flame, Send, +} from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import TikTokCarousel from '@/components/lp/shared/TikTokCarousel' +import SocialProofTicker from '@/components/lp/shared/SocialProofTicker' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const TIKTOK_PINK = '#FF0050' +const TIKTOK_CYAN = '#00F2EA' +const DARK_BG = '#121212' +const DARKER_BG = '#0A0A0A' + +const engagementMetrics = [ + { icon: Eye, value: '2.4M+', label: 'Views' }, + { icon: Heart, value: '340K+', label: 'Likes' }, + { icon: Share2, value: '89K+', label: 'Shares' }, + { icon: Bookmark, value: '156K+', label: 'Saves' }, +] + +const creatorCards = [ + { + handle: '@jessicatravels', + followers: '890K', + avatar: '/images/cdn/photo-1494790108377-be9c29b29330.jpg', + caption: 'OMG this Mexico deal is INSANE. $39/mo for all-inclusive?! I had to check for myself...', + likes: '45.2K', + comments: '3.1K', + shares: '12.8K', + }, + { + handle: '@couplesgetaway', + followers: '1.2M', + avatar: '/images/cdn/photo-1472099645785-5658abf4ff4e.jpg', + caption: 'We booked the $39/mo Mexico vacation everyone on TikTok is talking about. Here is what happened...', + likes: '78.9K', + comments: '5.6K', + shares: '21.3K', + }, + { + handle: '@budgetqueen', + followers: '2.1M', + avatar: '/images/cdn/photo-1438761681033-6461ffad8d80.jpg', + caption: 'STOP SCROLLING. This is not a drill. All-inclusive Mexico for $39/mo. I am literally shaking.', + likes: '124K', + comments: '8.9K', + shares: '34.7K', + }, +] + +const viralComments = [ + { user: '@sunshinevibes', text: 'Just booked!! Cannot wait omg', likes: 342 }, + { user: '@wanderlust.maya', text: 'Is this for REAL?! $39/mo??', likes: 891 }, + { user: '@travelwithmark', text: 'Did this last month. It is 100% legit. Cabo was incredible', likes: 1204 }, + { user: '@beachbum_sarah', text: 'My friend went and said it was the best vacation ever', likes: 567 }, + { user: '@deals.daily', text: 'This is the best travel deal on TikTok rn no cap', likes: 2341 }, + { user: '@vacay.mode', text: 'Just sent this to everyone I know lol', likes: 445 }, +] + +export default function LP30Influencer() { + const [activeComment, setActiveComment] = useState(0) + const [likeCount, setLikeCount] = useState(340892) + + useEffect(() => { + // Rotate comments + const commentInterval = setInterval(() => { + setActiveComment(prev => (prev + 1) % viralComments.length) + }, 3000) + + // Increment like count + const likeInterval = setInterval(() => { + setLikeCount(prev => prev + Math.floor(Math.random() * 5) + 1) + }, 2000) + + return () => { + clearInterval(commentInterval) + clearInterval(likeInterval) + } + }, []) + + const scrollToForm = () => { + document.getElementById('signup-form')?.scrollIntoView({ behavior: 'smooth' }) + } + + const TikTokLogo = ({ className = 'w-6 h-6' }: { className?: string }) => ( + + + + ) + + return ( +
+ + + + {/* ===== HERO ===== */} +
+ {/* Gradient orbs */} +
+
+ +
+ {/* TikTok badge */} +
+ + AS SEEN ON TIKTOK + +
+ + {/* TikTok icon */} +
+
+ +
+
+ LIVE +
+
+ +

+ The Vacation{' '} + + Everyone's Talking About + +

+ +

+ 2.4 million views. 340K likes. And counting. +

+

+ $59/mo{' '} + + ${PAYMENT_CONFIG.monthlyPrice}/mo + {' '} + — 5 days/4 nights all-inclusive Mexico +

+ + +
+
+ + {/* ===== ENGAGEMENT METRICS ===== */} +
+
+ {engagementMetrics.map((m, i) => ( +
+ +

{m.value}

+

{m.label}

+
+ ))} +
+
+ + {/* ===== SOCIAL PROOF TICKER ===== */} + + + {/* ===== TIKTOK CAROUSEL — MAIN CONTENT ===== */} +
+ +
+ + {/* ===== VIRAL PRICING COUNTDOWN ===== */} +
+
+

+ Viral pricing available for +

+
+ +
+

+ $59/mo{' '} + ${PAYMENT_CONFIG.monthlyPrice}/mo{' '} + — only while the timer lasts +

+
+
+ + {/* ===== CREATOR PROFILE CARDS ===== */} +
+
+

+ Creators Are{' '} + + Obsessed + +

+

+ Here's what influencers are posting about Mexico Paradise +

+ +
+ {creatorCards.map((creator, i) => ( +
+ {/* Creator header */} +
+ {creator.handle} +
+

{creator.handle}

+

{creator.followers} followers

+
+ +
+ + {/* Video thumbnail placeholder */} +
+ Vacation +
+
+ +
+
+ {/* Caption overlay */} +
+

{creator.caption}

+
+
+ + {/* Engagement bar */} +
+ + + {creator.likes} + + + + {creator.comments} + + + + {creator.shares} + + + + Save + +
+
+ ))} +
+
+
+ + {/* ===== VIRAL COMMENTS ===== */} +
+
+

+ + Comments Going Wild +

+ +
+ {viralComments.map((comment, i) => ( +
+
+ {comment.user.charAt(1).toUpperCase()} +
+
+

{comment.user}

+

{comment.text}

+
+
+ + {comment.likes.toLocaleString()} +
+
+ ))} +
+
+
+ + {/* ===== WHAT'S INCLUDED ===== */} +
+
+

+ What You Get for{' '} + + ${PAYMENT_CONFIG.monthlyPrice}/mo + +

+ +
+ {[ + '5 days / 4 nights all-inclusive', + 'Cancun, Cabo, Riviera Maya, or PV', + 'All meals + unlimited drinks', + 'Resort pools, beaches, amenities', + 'Bring a guest — included free', + '18 months to choose your dates', + '30-day money-back guarantee', + 'Flexible rescheduling', + ].map((item, i) => ( +
+ + {item} +
+ ))} +
+
+
+ + {/* ===== TESTIMONIALS ===== */} +
+
+

+ Real Travelers. Real Stories. +

+
+ {TESTIMONIALS.slice(0, 3).map((t, i) => ( + + ))} +
+
+
+ + {/* ===== FORM ===== */} +
+ {/* Gradient orbs */} +
+
+ +
+
+
+ + VIRAL PRICING — LIMITED TIME +
+

+ Book the Vacation Everyone's Talking About +

+

+ $59/mo{' '} + + ${PAYMENT_CONFIG.monthlyPrice}/mo + {' '} + — 5 days/4 nights all-inclusive +

+
+ + + +
+

+ + Secure checkout • 30-day money-back guarantee +

+
+ +
+ +
+
+
+ + {/* ===== FAQ ===== */} +
+
+

+ FAQ +

+ +
+
+ + {/* ===== FINAL CTA ===== */} +
+
+
+ +

+ Don't Just Watch the Videos — Live the Experience +

+

+ Viral pricing expires when the timer hits zero. Don't lose this. +

+ +
+
+
+ ) +} diff --git a/src/components/lp/pages/LP31BucketList.tsx b/src/components/lp/pages/LP31BucketList.tsx new file mode 100644 index 0000000..2ceca62 --- /dev/null +++ b/src/components/lp/pages/LP31BucketList.tsx @@ -0,0 +1,470 @@ +'use client' + +import { useState } from 'react' +import { + Check, MapPin, Palmtree, Sun, Waves, Camera, + GlassWater, Compass, Star, Clock, ArrowRight, + Sparkles, Heart, Shield, +} from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import DestinationCarousel from '@/components/lp/shared/DestinationCarousel' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import SocialProofTicker from '@/components/lp/shared/SocialProofTicker' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const ORANGE = '#F97316' +const DARK = '#1C1917' +const WARM_WHITE = '#FFFBEB' + +const bucketListItems = [ + { icon: Sun, text: 'Watch a Mexican sunset from your private balcony', checked: false }, + { icon: Waves, text: 'Swim in an ancient cenote surrounded by jungle', checked: false }, + { icon: GlassWater, text: 'Sip unlimited all-inclusive cocktails on the beach', checked: false }, + { icon: Camera, text: 'Take photos at ancient Mayan ruins', checked: false }, + { icon: Compass, text: 'Explore hidden beaches only locals know about', checked: false }, + { icon: Palmtree, text: 'Fall asleep to the sound of ocean waves', checked: false }, + { icon: Heart, text: 'Create memories that last a lifetime', checked: false }, + { icon: Star, text: 'Stay at a 5-star all-inclusive resort', checked: false }, +] + +const dailyCosts = [ + { label: 'Morning coffee', cost: '$5.50' }, + { label: 'Lunch out', cost: '$14.00' }, + { label: 'Streaming service', cost: '$1.80' }, + { label: 'This vacation', cost: '$1.30', highlight: true }, +] + +export default function LP31BucketList() { + const [checkedItems, setCheckedItems] = useState( + new Array(bucketListItems.length).fill(false) + ) + + const toggleItem = (index: number) => { + setCheckedItems(prev => { + const next = [...prev] + next[index] = !next[index] + return next + }) + } + + const checkedCount = checkedItems.filter(Boolean).length + + return ( +
+ + + {/* Hero */} +
+
+ Dramatic Mexican landscape with ancient ruins +
+
+ +
+
+ + YOUR ADVENTURE AWAITS +
+ +

+ Life's Too Short +
+ for “Someday” +

+ +

+ Stop scrolling through travel photos wishing you were there. + It's time to check off your Mexico bucket list. +

+ +

+ 5 days, 4 nights, all-inclusive — starting at just{' '} + $59/mo{' '} + + ${PAYMENT_CONFIG.monthlyPrice}/mo + +

+ + + Start Checking Off Your List + +
+ +
+ + + +
+
+ + + + {/* Interactive Bucket List */} +
+
+
+

+ Your Mexico Bucket List +

+

+ Tap to check off your dream experiences — then make them all happen +

+
+ +
+ {bucketListItems.map((item, index) => ( + + ))} +
+ + {/* Progress Indicator */} +
+
+ + {checkedCount}/{bucketListItems.length} checked + +
+
+
+
+
+ +
+

+ Check them ALL off for{' '} + $1.30/day +

+

+ That's less than a cup of coffee. Every. Single. Day. +

+

+ $59/mo →{' '} + + ${PAYMENT_CONFIG.monthlyPrice}/mo x {PAYMENT_CONFIG.totalMonths} months + {' '} + or ${PAYMENT_CONFIG.oneTimePrice} one-time +

+
+
+
+ + {/* Destination Carousel */} +
+
+
+

+ Choose Your Adventure +

+

+ Four stunning destinations, one unbeatable price +

+
+ +
+
+ + {/* Daily Cost Comparison */} +
+
+

+ What Does $1.30/Day Look Like? +

+ +
+ {dailyCosts.map((item) => ( +
+

+ {item.cost} +

+

+ {item.label} +

+ {item.highlight && ( +

PER DAY

+ )} +
+ ))} +
+ +

+ If you can afford a coffee, you can afford paradise. The question isn't + “can I?” — it's “why haven't I yet?” +

+
+
+ + {/* Countdown + Ebook Capture */} +
+
+ + +

+ Get the Complete Bucket List Guide +

+

+ Free e-book: “Budget Luxury Travel” — insider tips, packing lists, + and the best-kept secrets of Mexico +

+

+ Free guide + special pricing available for: +

+ +
+ +
+ +
+ +
+ +

+ We never share your email. Unsubscribe anytime. +

+
+
+ + {/* Pay Now */} +
+
+
+

+ Ready to Check Off Your List? +

+

+ Don't let “someday” turn into “never” +

+
+ +
+
+

+ $59/mo +

+ + ${PAYMENT_CONFIG.monthlyPrice} + + + /mo x {PAYMENT_CONFIG.totalMonths} months + +

+ or ${PAYMENT_CONFIG.oneTimePrice} one-time · 30-day money-back guarantee +

+
+ + +
+ + +
+
+ + {/* Testimonials */} +
+
+

+ They Checked Off Their List +

+
+ {TESTIMONIALS.slice(0, 3).map((t) => ( + + ))} +
+
+
+ + {/* FAQ */} + + +
+
+

+ Questions? We've Got Answers +

+ +
+
+ + {/* Final CTA */} +
+ +

+ “Someday” Is Today +

+

+ This price disappears when the timer hits zero. Don't miss it. +

+
+ +
+
+ +
+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP32DealBreaker.tsx b/src/components/lp/pages/LP32DealBreaker.tsx new file mode 100644 index 0000000..d951f84 --- /dev/null +++ b/src/components/lp/pages/LP32DealBreaker.tsx @@ -0,0 +1,507 @@ +'use client' + +import { useState } from 'react' +import { + Check, X, TrendingDown, Shield, Zap, Award, + ArrowRight, Calculator, BadgeDollarSign, Star, + ThumbsUp, ChevronDown, AlertTriangle, +} from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import ComparisonTable from '@/components/lp/shared/ComparisonTable' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import SocialProofTicker from '@/components/lp/shared/SocialProofTicker' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const BLUE = '#1E40AF' +const RED = '#DC2626' +const GREEN = '#16A34A' +const LIGHT_BLUE = '#EFF6FF' +const LIGHT_GREEN = '#F0FDF4' + +interface CompetitorPrice { + name: string + price: number + allInclusive: boolean + paymentPlan: boolean + color: string +} + +const competitors: CompetitorPrice[] = [ + { name: 'Expedia', price: 2800, allInclusive: false, paymentPlan: false, color: '#F59E0B' }, + { name: 'Hotels.com', price: 2600, allInclusive: false, paymentPlan: false, color: RED }, + { name: 'Booking.com', price: 3100, allInclusive: false, paymentPlan: false, color: '#2563EB' }, + { name: 'Direct Booking', price: 3200, allInclusive: false, paymentPlan: false, color: '#7C3AED' }, +] + +const savingsFeatures = [ + { label: '5 Days / 4 Nights', us: true, them: true }, + { label: 'All Meals Included', us: true, them: false }, + { label: 'Unlimited Drinks', us: true, them: false }, + { label: 'Resort Amenities', us: true, them: true }, + { label: 'Payment Plan ($39/mo)', us: true, them: false }, + { label: '30-Day Money Back', us: true, them: false }, + { label: 'Flexible Dates (18 mo)', us: true, them: false }, + { label: 'Price Guarantee', us: true, them: false }, +] + +export default function LP32DealBreaker() { + const [showCalculator, setShowCalculator] = useState(false) + const averageCompetitorPrice = Math.round( + competitors.reduce((sum, c) => sum + c.price, 0) / competitors.length + ) + const savings = averageCompetitorPrice - PAYMENT_CONFIG.oneTimePrice + + return ( +
+ + + {/* Hero */} +
+
+
+
+ +
+
+ + LOWEST PRICE GUARANTEE +
+ +

+ We Dare You to Find +
+ a Better Deal +

+ +

+ We compared our price to every major booking platform. + The result? It's not even close. +

+ +
+
+

Average OTA Price

+

+ ${averageCompetitorPrice.toLocaleString()} +

+
+ +
+

Our Price

+

+ ${PAYMENT_CONFIG.oneTimePrice} +

+
+
+ + + See the Full Comparison + +
+
+ + + + {/* Price Comparison Bars */} +
+
+
+

+ Side-by-Side Price Comparison +

+

+ Same destination. Same dates. Same quality. Wildly different prices. +

+
+ + {/* Visual Bar Chart */} +
+ {competitors.map((comp) => { + const percentage = (comp.price / 3500) * 100 + return ( +
+
+ + {comp.name} + +
+
+
+ + ${comp.price.toLocaleString()} + +
+
+ +
+ ) + })} + + {/* Us */} +
+
+ + Mexico Paradise + +
+
+
+ + ${PAYMENT_CONFIG.oneTimePrice} + +
+
+ +
+
+ + {/* Savings callout */} +
+ +

+ You Save ${savings.toLocaleString()} on Average +

+

+ That's a + {Math.round((savings / averageCompetitorPrice) * 100)}% discount + {' '} + compared to major booking platforms. Same resort. Same dates. Fraction of the price. +

+
+
+
+ + {/* Feature Comparison */} +
+
+
+

+ It's Not Just Cheaper — It's Better +

+

+ More features, better value, lower price. That's the trifecta. +

+
+ + + +
+
+

+ What's included that others charge extra for: +

+
+ {savingsFeatures.map((f) => ( +
+
+ {f.us ? ( + + ) : ( + + )} +
+ + {f.label} + +
+ ))} +
+
+
+
+
+ + {/* Savings Calculator */} +
+
+
+ +

+ Let's Break Down the Value +

+
+ +
+
+
+

+ What you'd pay separately: +

+
+ {[ + { item: '4 nights at resort', cost: '$1,200' }, + { item: 'All meals (5 days)', cost: '$750' }, + { item: 'Unlimited drinks', cost: '$400' }, + { item: 'Resort amenities', cost: '$350' }, + { item: 'Airport transfers', cost: '$150' }, + { item: 'Activities & entertainment', cost: '$200' }, + ].map((row) => ( +
+ {row.item} + + {row.cost} + +
+ ))} +
+ Total Value + $3,050 +
+
+
+ +
+

+ You pay +

+

+ ${PAYMENT_CONFIG.oneTimePrice} +

+

+ or just ${PAYMENT_CONFIG.monthlyPrice}/mo x {PAYMENT_CONFIG.totalMonths} +

+
+ + + Save $2,651 (87% off) + +
+
+
+
+
+
+ + {/* Price Match Guarantee */} +
+
+ +

+ Show Us a Better Price — We'll Match It +

+

+ We're so confident this is the best deal you'll find that we guarantee it. + Find a comparable all-inclusive package for less, and we'll match their price. +

+
+ + Best Price Guarantee + + + 30-Day Refund Policy + +
+
+
+ + {/* Ebook Capture */} +
+
+ +

+ Get the Full Price Comparison Report +

+

+ Free e-book: “Budget Luxury Travel” with detailed price breakdowns + across all major booking platforms +

+

+ Special pricing expires in: +

+ +
+ +
+ +
+ +
+
+
+ + {/* Pay Now */} +
+
+
+

+ Lock In the Best Price +

+

+ You won't find this deal anywhere else. We guarantee it. +

+
+ +
+
+
+ BEST VALUE +
+

+ $59/mo +

+ + ${PAYMENT_CONFIG.monthlyPrice} + + + /mo x {PAYMENT_CONFIG.totalMonths} months + +

+ or ${PAYMENT_CONFIG.oneTimePrice} one-time · 30-day money-back guarantee +

+
+ + +
+ + +
+
+ + {/* Testimonials */} +
+
+

+ Smart Travelers Who Found the Best Deal +

+
+ {TESTIMONIALS.slice(0, 3).map((t) => ( + + ))} +
+
+
+ + + + {/* FAQ */} +
+
+

+ Frequently Asked Questions +

+ +
+
+ + {/* Final CTA */} +
+ +

+ The Numbers Don't Lie +

+

+ ${PAYMENT_CONFIG.oneTimePrice} for what others charge $3,000+. + This promotional price ends when the timer hits zero. +

+
+ +
+
+ +
+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP33EscapePlan.tsx b/src/components/lp/pages/LP33EscapePlan.tsx new file mode 100644 index 0000000..2881a9c --- /dev/null +++ b/src/components/lp/pages/LP33EscapePlan.tsx @@ -0,0 +1,518 @@ +'use client' + +import { useState } from 'react' +import { + Shield, Target, MapPin, Clock, CheckCircle2, + ChevronRight, Lock, Crosshair, Radio, + Plane, Palmtree, Sun, Waves, Eye, Zap, + ArrowRight, AlertTriangle, +} from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import SocialProofTicker from '@/components/lp/shared/SocialProofTicker' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, DESTINATIONS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const DARK = '#111827' +const GREEN = '#22C55E' +const DARK_GREEN = '#15803D' +const TERMINAL_BG = '#0A0F1A' + +const missionSteps = [ + { + phase: 'PHASE 01', + title: 'INTELLIGENCE GATHERING', + description: 'Download the free classified travel dossier. Contains insider intel on luxury Mexico vacations at deep-cover prices.', + icon: Eye, + status: 'READY', + }, + { + phase: 'PHASE 02', + title: 'SELECT YOUR DESTINATION', + description: 'Choose from 4 confirmed safe houses: Cancun, Cabo, Riviera Maya, or Puerto Vallarta. All 5-star. All all-inclusive.', + icon: MapPin, + status: 'PENDING', + }, + { + phase: 'PHASE 03', + title: 'SECURE YOUR CERTIFICATE', + description: 'Lock in the $29/mo payment plan or deploy $249 in a single strike. 30-day extraction guarantee if the mission doesn\'t meet expectations.', + icon: Lock, + status: 'PENDING', + }, + { + phase: 'PHASE 04', + title: 'EXECUTE THE ESCAPE', + description: 'Book your dates within 18 months. Pack your bags. Leave the office behind. Mission complete.', + icon: Plane, + status: 'PENDING', + }, +] + +const missionBriefing = [ + { label: 'MISSION', value: 'Get out of your office and onto a beach' }, + { label: 'OBJECTIVE', value: 'All-inclusive paradise for $1.30/day' }, + { label: 'DURATION', value: '5 days / 4 nights' }, + { label: 'COVER', value: 'Luxury resort guest' }, + { label: 'CLEARANCE', value: 'All-inclusive (meals, drinks, amenities)' }, + { label: 'BUDGET', value: `$${PAYMENT_CONFIG.monthlyPrice}/mo x ${PAYMENT_CONFIG.totalMonths} or $${PAYMENT_CONFIG.oneTimePrice} total` }, +] + +const targetLocations = DESTINATIONS.map((d) => ({ + name: d.name, + image: d.images[0], + tagline: d.tagline, + codename: d.name.toUpperCase().replace(/\s+/g, '-'), +})) + +export default function LP33EscapePlan() { + const [activeStep, setActiveStep] = useState(0) + + return ( +
+ + + {/* Hero */} +
+
+ Beach paradise escape destination +
+ {/* Scan lines effect */} +
+
+ +
+
+ + CLASSIFIED // TOP SECRET // EYES ONLY +
+ +

+ Your Escape Plan +

+ +
+

+ > INITIATING ESCAPE SEQUENCE... +

+

+ > TARGET: All-inclusive Mexico resort +

+

+ > COST: $59/mo{' '} + ${PAYMENT_CONFIG.monthlyPrice}/mo x {PAYMENT_CONFIG.totalMonths} +

+

+ > DURATION: 5 days / 4 nights +

+

+ > STATUS: AWAITING YOUR COMMAND_ +

+
+ + + Begin Mission + +
+
+ + + + {/* Mission Briefing */} +
+
+
+ +

+ Mission Briefing +

+

+ // CLASSIFICATION: FOR YOUR EYES ONLY +

+
+ +
+ {missionBriefing.map((item, i) => ( +
+ + {item.label}: + + {item.value} +
+ ))} +
+
+
+ + {/* Mission Steps */} +
+
+
+

+ Operation Paradise +

+

+ Follow the mission objectives to secure your escape +

+
+ +
+ {missionSteps.map((step, index) => ( + + ))} +
+
+
+ + {/* Target Locations */} +
+
+
+

+ Target Locations +

+

+ // SELECT YOUR EXTRACTION POINT +

+
+ +
+ {targetLocations.map((loc) => ( +
+
+ {loc.name} +
+
+

+ CODENAME: {loc.codename} +

+

+ {loc.name} +

+

{loc.tagline}

+
+
+
+ ))} +
+
+
+ + {/* Countdown + Ebook */} +
+
+ + +

+ Download Your Escape Plan +

+

+ Free classified dossier: “Budget Luxury Travel” — everything you need + to execute your escape to paradise +

+

+ MISSION WINDOW CLOSES IN: +

+ +
+ +
+ +
+ +
+
+
+ + {/* Pay Now */} +
+
+
+

+ Execute Mission +

+

+ // SECURE YOUR VACATION CERTIFICATE NOW +

+
+ +
+
+

$59/mo

+ + ${PAYMENT_CONFIG.monthlyPrice} + + + /mo x {PAYMENT_CONFIG.totalMonths} months + +

+ or ${PAYMENT_CONFIG.oneTimePrice} one-time · 30-day extraction guarantee +

+
+ + +
+ + +
+
+ + {/* Testimonials */} +
+
+

+ Successful Operatives +

+
+ {TESTIMONIALS.slice(0, 3).map((t) => ( + + ))} +
+
+
+ + {/* FAQ */} + + +
+
+

+ Mission Intel +

+ +
+
+ + {/* Final CTA */} +
+ +

+ The Clock Is Ticking, Agent +

+

+ MISSION WINDOW CLOSING IN: +

+
+ +
+
+ +
+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP34TikTokVibes.tsx b/src/components/lp/pages/LP34TikTokVibes.tsx new file mode 100644 index 0000000..8a5cfe5 --- /dev/null +++ b/src/components/lp/pages/LP34TikTokVibes.tsx @@ -0,0 +1,471 @@ +'use client' + +import { + Heart, MessageCircle, Share2, Bookmark, Music2, + ArrowRight, Sparkles, Eye, Flame, TrendingUp, + Shield, Star, Play, Users, Zap, +} from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import TikTokCarousel from '@/components/lp/shared/TikTokCarousel' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import SocialProofTicker from '@/components/lp/shared/SocialProofTicker' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const BLACK = '#000000' +const CYAN = '#25F4EE' +const PINK = '#FE2C55' +const DARK_BG = '#121212' + +const engagementStats = [ + { icon: Eye, label: 'views', value: '12.3M', color: 'white' }, + { icon: Heart, label: 'likes', value: '847K', color: PINK }, + { icon: MessageCircle, label: 'comments', value: '156K', color: CYAN }, + { icon: Share2, label: 'shares', value: '23K', color: 'white' }, +] + +const creatorReactions = [ + { + handle: '@travelwithsoph', + reaction: 'NO WAY this is only $39/mo for ALL INCLUSIVE??', + likes: '234K', + avatar: '/images/cdn/photo-1494790108377-be9c29b29330.jpg', + }, + { + handle: '@budgetluxury_', + reaction: 'I literally booked this after seeing one TikTok. Best decision ever.', + likes: '189K', + avatar: '/images/cdn/photo-1472099645785-5658abf4ff4e.jpg', + }, + { + handle: '@mexicovibes.co', + reaction: 'The cenote photos are INSANE. $1.30/day for 5-star? STOPPPP.', + likes: '312K', + avatar: '/images/cdn/photo-1438761681033-6461ffad8d80.jpg', + }, + { + handle: '@thecheaptraveler', + reaction: 'POV: You find the Mexico vacation deal that ACTUALLY delivers', + likes: '445K', + avatar: '/images/cdn/photo-1500648767791-00dcc994a43e.jpg', + }, +] + +const trendingHashtags = [ + '#MexicoParadise', '#AllInclusive', '#TravelTikTok', + '#BudgetLuxury', '#VacationDeal', '#CancunTikTok', + '#ResortLife', '#TravelHack', +] + +export default function LP34TikTokVibes() { + return ( +
+ + + {/* Hero */} +
+
+ Stunning Mexico beach resort +
+
+ +
+ {/* TikTok-style badge */} +
+ + TRENDING NOW + +
+ +

+ The Mexico deal +
+ that broke{' '} + TikTok +

+ +

+ 12.3 million views. 847K likes. One vacation deal that's going viral + for a reason. +

+ + {/* Engagement Stats */} +
+ {engagementStats.map((stat) => ( +
+
+ + + {stat.value} + +
+

{stat.label}

+
+ ))} +
+ +
+ Swipe through real TikToks below + 👇 +
+ + + Get the Insider Guide + +
+
+ + {/* TikTok Carousel - Main Feature */} + + + {/* Creator Reactions */} +
+
+
+

+ Creator Reactions +

+

+ What TikTok creators are saying about this deal +

+
+ +
+ {creatorReactions.map((creator) => ( +
+ {creator.handle} +
+

+ {creator.handle} +

+

+ “{creator.reaction}” +

+
+ + {creator.likes} + +
+
+
+ ))} +
+ + {/* Trending Hashtags */} +
+ {trendingHashtags.map((tag) => ( + + {tag} + + ))} +
+
+
+ + {/* The Deal Section */} +
+
+

+ Why 12 Million People +
+ Can't Look Away +

+ +
+ {[ + { label: '5 Days / 4 Nights', icon: '🏖️' }, + { label: 'All-Inclusive', icon: '🍹' }, + { label: '5-Star Resort', icon: '⭐' }, + { label: '$1.30/Day', icon: '🤯' }, + ].map((item) => ( +
+ {item.icon} +

{item.label}

+
+ ))} +
+ +
+

+ The math that made this go viral: +

+
+
+

Regular price

+

+ $3,000+ +

+
+ +
+

Our price

+

+ ${PAYMENT_CONFIG.oneTimePrice} +

+
+ +
+

Per day

+

+ $1.30 +

+
+
+

+ $59/mo →{' '} + + ${PAYMENT_CONFIG.monthlyPrice}/mo + {' '} + x {PAYMENT_CONFIG.totalMonths} months (promotional pricing) +

+
+
+
+ + {/* Ebook Capture */} +
+
+ + +

+ Want the Insider Tips +
+ These Creators Used? +

+

+ Free e-book: “Budget Luxury Travel” — the TikTok travel guide + that's been shared 23K+ times +

+

+ Free guide + promotional pricing expires in: +

+ +
+ +
+ +
+ +
+
+
+ + {/* Pay Now */} +
+
+
+

+ Ready to Go Viral on Vacation? +

+

+ Join 23K+ people who already claimed this deal +

+
+ +
+
+
+ VIRAL DEAL +
+

$59/mo

+ + ${PAYMENT_CONFIG.monthlyPrice} + + + /mo x {PAYMENT_CONFIG.totalMonths} months + +

+ or ${PAYMENT_CONFIG.oneTimePrice} one-time · 30-day money-back guarantee +

+
+ + +
+ + +
+
+ + {/* Testimonials */} +
+
+

+ Real Reviews from Real Travelers +

+
+ {TESTIMONIALS.slice(0, 3).map((t) => ( + + ))} +
+
+
+ + {/* FAQ */} + + +
+
+

+ The FAQ Section +

+ +
+
+ + {/* Final CTA */} +
+ +

+ Don't Just Watch the TikToks — Live It +

+

+ This deal is going fast. Promotional pricing ends when the timer hits zero. +

+
+ +
+
+ +
+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP35NoBrainer.tsx b/src/components/lp/pages/LP35NoBrainer.tsx new file mode 100644 index 0000000..b119232 --- /dev/null +++ b/src/components/lp/pages/LP35NoBrainer.tsx @@ -0,0 +1,569 @@ +'use client' + +import { + Calculator, Check, ArrowRight, TrendingDown, + Shield, Sparkles, Coffee, Tv, UtensilsCrossed, + Palmtree, Star, Zap, Brain, ChevronRight, + DollarSign, Equal, +} from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import SocialProofTicker from '@/components/lp/shared/SocialProofTicker' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const WHITE = '#FFFFFF' +const INDIGO = '#4F46E5' +const GREEN = '#10B981' +const LIGHT_INDIGO = '#EEF2FF' +const LIGHT_GREEN = '#ECFDF5' + +const dailyCostComparisons = [ + { + label: 'Morning Coffee', + cost: 5.50, + icon: Coffee, + frequency: 'daily', + color: '#92400E', + bgColor: '#FEF3C7', + }, + { + label: 'Lunch Out', + cost: 12.00, + icon: UtensilsCrossed, + frequency: 'daily', + color: '#9F1239', + bgColor: '#FFE4E6', + }, + { + label: 'Streaming Services', + cost: 1.80, + icon: Tv, + frequency: 'daily', + color: '#5B21B6', + bgColor: '#EDE9FE', + }, + { + label: 'THIS VACATION', + cost: 1.30, + icon: Palmtree, + frequency: 'daily', + color: WHITE, + bgColor: GREEN, + highlight: true, + }, +] + +const mathBreakdown = [ + { label: '$1.30/day', sublabel: 'daily cost' }, + { label: '300 days', sublabel: 'payment period' }, + { label: '$390', sublabel: 'total investment' }, + { label: '5-day luxury vacation', sublabel: 'what you get' }, +] + +const whatYouGet = [ + '5 days / 4 nights at a 5-star resort', + 'All meals included (breakfast, lunch, dinner)', + 'Unlimited drinks (including alcohol)', + 'Resort pools, beach, and amenities', + 'Evening entertainment and live shows', + '18 months to book your travel dates', + '30-day money-back guarantee', + 'Flexible payment plan available', +] + +export default function LP35NoBrainer() { + return ( +
+ + + {/* Hero */} +
+
+
+
+ +
+
+ + SIMPLE MATH. INCREDIBLE VALUE. +
+ +

+ Let's Do the Math. +
+ It's a No-Brainer. +

+ +

+ We're about to show you why this is the easiest decision you'll make all year. + No tricks. Just math. +

+ + {/* Hero Math */} +
+
+ {mathBreakdown.map((item, index) => ( +
+
+

+ {item.label} +

+

+ {item.sublabel} +

+
+ {index < mathBreakdown.length - 1 && ( + + {index === mathBreakdown.length - 2 ? '=' : '\u00D7'} + + )} +
+ ))} +
+
+ + + See the Full Breakdown + +
+
+ + + + {/* Daily Cost Comparison - Big Bold Cards */} +
+
+
+

+ If You Can Afford a Coffee, +
+ You Can Afford Paradise +

+

+ Here's what $1.30/day looks like compared to things you already spend on +

+
+ +
+ {dailyCostComparisons.map((item) => ( +
+ {item.highlight && ( +
+ BEST VALUE +
+ )} +
+ +
+

+ ${item.cost.toFixed(2)} +

+

+ {item.label} +

+

+ per day +

+
+ ))} +
+ + {/* Visual Bar Chart */} +
+

+ Daily Cost Comparison +

+
+ {dailyCostComparisons.map((item) => { + const maxCost = 12 + const widthPercent = (item.cost / maxCost) * 100 + return ( +
+
+ + {item.label} + +
+
+
+ + ${item.cost.toFixed(2)} + +
+
+
+ ) + })} +
+
+
+
+ + {/* The Math Proof */} +
+
+
+ +

+ The Logic Is Simple +

+

+ Here's the value breakdown that makes this a no-brainer +

+
+ +
+ {/* What you pay */} +
+

+ What You Pay +

+
+
+

Option A: Payment Plan

+

$59/mo

+

+ ${PAYMENT_CONFIG.monthlyPrice}/mo +

+

+ x {PAYMENT_CONFIG.totalMonths} months = ${PAYMENT_CONFIG.totalPrice} +

+
+
+

or

+
+
+

Option B: One-Time

+

+ ${PAYMENT_CONFIG.oneTimePrice} +

+
+
+
+ + {/* What you get */} +
+

+ What You Get ($3,000+ Value) +

+
+ {whatYouGet.map((item) => ( +
+ + {item} +
+ ))} +
+
+
+ + {/* Savings callout */} +
+

+ You Save Over $2,600 +

+

+ That's an 87% discount on a $3,000+ vacation. The math doesn't lie. +

+
+
+
+ + {/* The "Can I Afford It" Section */} +
+
+

+ “But Can I Really Afford It?” +

+ +
+

+ Let's check. Do you spend money on any of these? +

+ +
+ {[ + { text: 'Coffee 3x/week', savings: '$9/wk' }, + { text: 'Fast food once/week', savings: '$12/wk' }, + { text: 'Streaming subscriptions', savings: '$15/mo' }, + { text: 'Gas station snacks', savings: '$5/wk' }, + { text: 'Impulse Amazon buys', savings: '$20/mo' }, + { text: 'Uber Eats delivery fees', savings: '$8/wk' }, + ].map((item) => ( +
+

+ {item.text} +

+

+ ~ {item.savings} +

+
+ ))} +
+ +

+ If you checked even ONE — you can afford this vacation. +

+

+ ${PAYMENT_CONFIG.monthlyPrice}/mo is less than what most people spend on coffee in a week. +

+
+
+
+ + {/* Ebook Capture */} +
+
+ + +

+ Get the Full Cost Breakdown +

+

+ Free e-book: “Budget Luxury Travel” — detailed pricing analysis, + savings tips, and the complete math behind the deal +

+

+ Free guide + promotional pricing available for: +

+ +
+ +
+ +
+ +
+
+
+ + {/* Pay Now */} +
+
+
+

+ The Math Checks Out +

+

+ $1.30/day. 5-star resort. All-inclusive. It really is that simple. +

+
+ +
+
+
+ NO-BRAINER DEAL +
+

+ $59/mo +

+ + ${PAYMENT_CONFIG.monthlyPrice} + + + /mo x {PAYMENT_CONFIG.totalMonths} months + +

+ or ${PAYMENT_CONFIG.oneTimePrice} one-time · 30-day money-back guarantee +

+

+ = $1.30/day for a luxury Mexico vacation +

+
+ + +
+ + +
+
+ + {/* Testimonials */} +
+
+

+ Smart People Who Did the Math +

+
+ {TESTIMONIALS.slice(0, 3).map((t) => ( + + ))} +
+
+
+ + + + {/* FAQ */} +
+
+

+ Frequently Asked Questions +

+ +
+
+ + {/* Final CTA */} +
+ +

+ $1.30/Day. 5-Star Resort. All-Inclusive. +

+

+ The math is clear. The deal expires when the timer hits zero. +

+
+ +
+
+ +
+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP36WeekendEscape.tsx b/src/components/lp/pages/LP36WeekendEscape.tsx new file mode 100644 index 0000000..baf707c --- /dev/null +++ b/src/components/lp/pages/LP36WeekendEscape.tsx @@ -0,0 +1,493 @@ +'use client' + +import { useState } from 'react' +import { + Calendar, Sun, Plane, MapPin, Clock, CheckCircle2, + ArrowRight, Sparkles, Star, ChevronRight, Heart, + CalendarDays, CalendarCheck, Palmtree, Umbrella, + Coffee, Sunset, PartyPopper, Shield, +} from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import DestinationCarousel from '@/components/lp/shared/DestinationCarousel' +import SocialProofTicker from '@/components/lp/shared/SocialProofTicker' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const SKY_BLUE = '#0EA5E9' +const ORANGE = '#F97316' +const WHITE = '#FFFFFF' + +const calendarDays = [ + { day: 'Mon', date: 12, type: 'normal' }, + { day: 'Tue', date: 13, type: 'normal' }, + { day: 'Wed', date: 14, type: 'vacation' }, + { day: 'Thu', date: 15, type: 'vacation' }, + { day: 'Fri', date: 16, type: 'vacation' }, + { day: 'Sat', date: 17, type: 'vacation' }, + { day: 'Sun', date: 18, type: 'vacation' }, + { day: 'Mon', date: 19, type: 'normal' }, + { day: 'Tue', date: 20, type: 'normal' }, +] + +const flexFeatures = [ + { + icon: CalendarDays, + title: 'Pick Any 5 Days', + description: 'No fixed dates. Choose when YOU want to go within 18 months.', + }, + { + icon: Plane, + title: 'Any Airport, Any Airline', + description: 'Fly from wherever is most convenient. We handle the resort.', + }, + { + icon: MapPin, + title: '4 Stunning Destinations', + description: 'Cancun, Cabo, Riviera Maya, or Puerto Vallarta — your pick.', + }, + { + icon: Umbrella, + title: 'All-Inclusive Everything', + description: 'Food, drinks, activities, pools, beach — all included.', + }, +] + +const weekendIdeas = [ + { + title: 'The Long Weekend', + dates: 'Thu–Mon', + description: 'Take 2 days off, get a 5-day paradise escape. Back by Tuesday.', + image: '/images/cdn/photo-1507525428034-b723cf961d3e.jpg', + }, + { + title: 'The Mid-Week Reset', + dates: 'Mon–Fri', + description: 'Skip one work week. Come back completely recharged.', + image: '/images/cdn/photo-1510097467424-192d713fd8b2.jpg', + }, + { + title: 'The Holiday Extension', + dates: 'Around any holiday', + description: 'Attach your trip to a long weekend. Maximize your PTO.', + image: '/images/cdn/photo-1512100356356-de1b84283e18.jpg', + }, + { + title: 'The Celebration Trip', + dates: 'Any special date', + description: 'Birthday? Anniversary? Make it unforgettable in Mexico.', + image: '/images/cdn/photo-1581710862235-eb6e05d8783f.jpg', + }, +] + +const dailyBreakdown = [ + { icon: Coffee, label: 'Morning', text: 'Wake up to ocean views, gourmet breakfast buffet' }, + { icon: Sun, label: 'Afternoon', text: 'Pool, beach, snorkeling, spa — your choice' }, + { icon: Sunset, label: 'Evening', text: 'Sunset cocktails, fine dining, live entertainment' }, + { icon: PartyPopper, label: 'Night', text: 'Dance, stargaze, or just listen to the waves' }, +] + +export default function LP36WeekendEscape() { + const [selectedDay, setSelectedDay] = useState(null) + + return ( +
+ + + {/* Hero Section */} +
+
+
+
+
+ +
+
+ + Limited Time: Special Weekend Pricing +
+ +

+ Turn Any Week Into +
+ + Paradise + + +

+ +

+ 5 days & 4 nights all-inclusive in Mexico. +
+ Pick any 5 days. We handle the rest. +

+ +
+ $59/mo + ${PAYMENT_CONFIG.monthlyPrice}/mo + + SAVE 34% + +
+ + {/* Calendar Visual */} +
+
+

+ Your Calendar +

+ March 2026 +
+
+ {calendarDays.map((d, i) => ( + + ))} +
+
+ + Paradise days + + + Regular days + +
+
+ + + Plan My Escape + +
+
+ + + + {/* Flexibility Features */} +
+
+

+ Total Flexibility. Zero Stress. +

+

+ Stop waiting for the “perfect time.” With our flexible certificates, + any time becomes the perfect time. +

+ +
+ {flexFeatures.map((f, i) => ( +
+
+ +
+

+ {f.title} +

+

{f.description}

+
+ ))} +
+
+
+ + {/* Weekend Ideas */} +
+
+

+ 4 Ways to Plan Your Escape +

+

+ However you slice it, paradise fits into your schedule. +

+ +
+ {weekendIdeas.map((idea, i) => ( +
+
+ {idea.title} +
+ {idea.dates} +
+
+
+

+ {idea.title} +

+

{idea.description}

+
+
+ ))} +
+
+
+ + {/* Daily Breakdown */} +
+
+

+ A Day in Paradise +

+

+ Every day is designed for pure enjoyment. Here's a taste. +

+ +
+ {dailyBreakdown.map((item, i) => ( +
+
+ +
+
+

+ {item.label} +

+

{item.text}

+
+
+ ))} +
+
+
+ + {/* Pricing Anchor */} +
+
+

+ Less Than Your Daily Coffee +

+

+ At ${PAYMENT_CONFIG.monthlyPrice}/mo, that's just $1.30/day for 5 days of all-inclusive paradise. +

+ +
+
+

$3,200+

+

Typical all-inclusive vacation

+
+
+

$59/mo

+

Regular certificate price

+
+
+

${PAYMENT_CONFIG.monthlyPrice}/mo

+

Your price today

+
+
+ + +
+
+ + {/* Destination Carousel */} +
+
+

+ Choose Your Destination +

+

+ Four incredible Mexican destinations. All yours to explore. +

+ +
+
+ + {/* Testimonials */} +
+
+

+ Travelers Who Took the Leap +

+
+ {TESTIMONIALS.slice(0, 3).map((t, i) => ( + + ))} +
+
+
+ + {/* Primary CTA - Ebook */} +
+
+ +

+ Get the Date Planning Guide +

+

+ Our free “Budget Luxury Travel” ebook shows you exactly how to plan + the perfect getaway around your schedule. Download it now before this offer expires. +

+ + + +

+ Free instant download. No spam, ever. +

+
+
+ + {/* Secondary CTA - Pay Now */} +
+
+

+ Ready to Book? Lock In Your Rate +

+

+ Don't let this price slip away. Once the timer hits zero, the rate goes back to $59/mo. +

+

+ 30-day money-back guarantee. Cancel anytime. +

+ + +
+
+ + {/* Trust Badges */} +
+ +
+ + {/* FAQ */} + + +
+
+

+ Questions? We've Got Answers +

+ +
+
+ + {/* Final CTA Banner */} +
+

+ Your Calendar Deserves Some Color +

+

+ Stop scrolling. Start planning. 5 days of paradise are waiting for you. +

+ + Claim My Spot + +
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP37TrustFall.tsx b/src/components/lp/pages/LP37TrustFall.tsx new file mode 100644 index 0000000..1b54363 --- /dev/null +++ b/src/components/lp/pages/LP37TrustFall.tsx @@ -0,0 +1,459 @@ +'use client' + +import { + Star, Shield, CheckCircle2, Award, Users, ThumbsUp, + Quote, ArrowRight, BadgeCheck, MessageSquare, Eye, + Lock, Heart, TrendingUp, Verified, Clock, + ShieldCheck, Sparkles, ChevronRight, +} from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import TikTokCarousel from '@/components/lp/shared/TikTokCarousel' +import SocialProofTicker from '@/components/lp/shared/SocialProofTicker' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import ComparisonTable from '@/components/lp/shared/ComparisonTable' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const DARK_BLUE = '#1E3A5F' +const GOLD = '#F59E0B' +const WHITE = '#FFFFFF' + +const stats = [ + { value: '4.8', suffix: '★', label: 'Average Rating', icon: Star }, + { value: '2,847', suffix: '', label: 'Happy Travelers', icon: Users }, + { value: '98', suffix: '%', label: 'Satisfaction Rate', icon: ThumbsUp }, + { value: '30', suffix: '-day', label: 'Money-Back Guarantee', icon: Shield }, +] + +const trustPoints = [ + { + icon: ShieldCheck, + title: '30-Day Full Refund', + description: 'Not satisfied? Get 100% of your money back within 30 days. No questions, no hassle.', + }, + { + icon: Lock, + title: 'Secure Payment', + description: 'Bank-level 256-bit SSL encryption. Your financial data is always protected.', + }, + { + icon: BadgeCheck, + title: 'Verified Resorts', + description: 'Every resort is personally inspected and rated 4+ stars by our team.', + }, + { + icon: Verified, + title: 'Real Reviews Only', + description: 'Every review is from a verified guest. Zero fake testimonials, ever.', + }, + { + icon: Award, + title: 'BBB Accredited', + description: 'A+ rating with the Better Business Bureau since 2019.', + }, + { + icon: Heart, + title: 'Family-Owned', + description: 'Not a faceless corporation. Real people who care about your experience.', + }, +] + +const reviewHighlights = [ + { text: 'Best vacation deal we\'ve ever found', count: 847, stars: 5 }, + { text: 'The resort was even better than the photos', count: 623, stars: 5 }, + { text: 'Worth every penny — exceeded expectations', count: 512, stars: 5 }, + { text: 'Already planning our second trip', count: 489, stars: 5 }, + { text: 'Customer service was phenomenal', count: 394, stars: 5 }, + { text: 'All-inclusive really means ALL inclusive', count: 371, stars: 4 }, +] + +const ratingDistribution = [ + { stars: 5, percentage: 78, count: 2221 }, + { stars: 4, percentage: 16, count: 455 }, + { stars: 3, percentage: 4, count: 114 }, + { stars: 2, percentage: 1, count: 28 }, + { stars: 1, percentage: 1, count: 29 }, +] + +export default function LP37TrustFall() { + return ( +
+ + + {/* Hero Section */} +
+
+
+
+ +
+
+ {[1, 2, 3, 4, 5].map((s) => ( + + ))} +
+ +

+ Don't Trust Us. +
+ Trust 2,847 Travelers. +

+ +

+ 5 days & 4 nights all-inclusive in Mexico. See why thousands of travelers + rate us 4.8 out of 5 stars. +

+ +
+ $59/mo + ${PAYMENT_CONFIG.monthlyPrice}/mo + + SAVE 34% + +
+ + {/* Stats Bar */} +
+ {stats.map((s, i) => ( +
+ +

+ {s.value}{s.suffix} +

+

{s.label}

+
+ ))} +
+ + + See All Reviews + +
+
+ + + + {/* Rating Distribution */} +
+
+

+ The Numbers Don't Lie +

+

+ Based on 2,847 verified traveler reviews +

+ +
+ {/* Overall Rating */} +
+

4.8

+
+ {[1, 2, 3, 4, 5].map((s) => ( + + ))} +
+

2,847 reviews

+
+ + {/* Distribution Bars */} +
+ {ratingDistribution.map((r) => ( +
+
+ {r.stars} + +
+
+
+
+ {r.count} +
+ ))} +
+
+
+
+ + {/* Review Highlights */} +
+
+

+ What Travelers Say Most +

+

+ The most common themes from verified reviews +

+ +
+ {reviewHighlights.map((r, i) => ( +
+
+ {Array.from({ length: r.stars }).map((_, j) => ( + + ))} +
+

“{r.text}”

+

+ + Mentioned in {r.count} reviews +

+
+ ))} +
+
+
+ + {/* Full Testimonial Grid */} +
+
+

+ Real Stories From Real Travelers +

+

+ Every review is verified. Every traveler is real. +

+ +
+ {TESTIMONIALS.map((t, i) => ( + + ))} +
+
+
+ + {/* TikTok Videos */} +
+
+

+ Watch Real Travelers at Our Resorts +

+

+ Don't just take our word for it. See it for yourself. +

+ +
+
+ + {/* Trust Points */} +
+
+

+ Your Trust Is Everything +

+

+ We know you're careful with your money. Here's why we've earned the trust of thousands. +

+ +
+ {trustPoints.map((tp, i) => ( +
+
+ +
+

+ {tp.title} +

+

{tp.description}

+
+ ))} +
+
+
+ + {/* Trust Badges - Larger */} +
+
+ +
+
+ + {/* Comparison Table */} +
+
+

+ See How We Compare +

+

+ Side-by-side with traditional booking options +

+ +
+
+ + {/* Pricing + Countdown */} +
+
+

+ The Price Won't Last +

+

+ Regular price: $59/mo. Today only: + ${PAYMENT_CONFIG.monthlyPrice}/mo or + ${PAYMENT_CONFIG.oneTimePrice} one-time. +

+

+ 2,847 travelers trusted us. You can too. 30-day money-back guarantee. +

+ + +
+
+ + {/* FAQ */} +
+
+ + +

+ Still Have Questions? +

+

+ We believe in total transparency. Here are the most common questions. +

+ +
+
+ + {/* Primary CTA - Ebook */} +
+
+ +

+ See All 500+ Reviews +

+

+ Download our free “Budget Luxury Travel” guide and get access to our full + review collection. See exactly what 2,847 travelers experienced. +

+ + + +

+ Free instant download. Your data is protected. +

+
+
+ + {/* Secondary CTA - Pay Now */} +
+
+

+ Ready to Join 2,847 Happy Travelers? +

+

+ This price disappears when the timer hits zero. Don't lose your spot. +

+

+ 30-day full refund guarantee. Zero risk. +

+ + +
+
+ + {/* Final CTA */} +
+
+
+ {[1, 2, 3, 4, 5].map((s) => ( + + ))} +
+

+ 4.8 Stars. 2,847 Travelers. Your Turn. +

+ + Get Started Today + +
+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP38Sunrise.tsx b/src/components/lp/pages/LP38Sunrise.tsx new file mode 100644 index 0000000..8f60273 --- /dev/null +++ b/src/components/lp/pages/LP38Sunrise.tsx @@ -0,0 +1,513 @@ +'use client' + +import { + Sun, Sunrise, Waves, Wind, Music, Utensils, + Heart, Sparkles, Eye, Palette, ArrowRight, + Star, CloudSun, Shell, Flower2, Coffee, + GlassWater, TreePalm, Camera, +} from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import PricingDisplay from '@/components/lp/shared/PricingDisplay' +import SocialProofTicker from '@/components/lp/shared/SocialProofTicker' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const SUNRISE_ORANGE = '#FB923C' +const WARM_PINK = '#F472B6' +const GOLD = '#FBBF24' +const CREAM = '#FFFBEB' + +const sensoryExperiences = [ + { + sense: 'See', + icon: Eye, + title: 'Sunrises That Take Your Breath Away', + description: 'Watch the sky paint itself in shades of gold, coral, and lavender as the sun rises over the Caribbean.', + image: '/images/cdn/photo-1506929562872-bb421503ef21.jpg', + }, + { + sense: 'Hear', + icon: Waves, + title: 'The Rhythm of the Ocean', + description: 'Fall asleep to gentle waves. Wake up to birdsong. No alarm clocks. No traffic. Just nature.', + image: '/images/cdn/photo-1507525428034-b723cf961d3e.jpg', + }, + { + sense: 'Taste', + icon: Utensils, + title: 'Flavors You\'ll Dream About', + description: 'Fresh ceviche by the pool. Authentic mole at dinner. Exotic cocktails at sunset. All included.', + image: '/images/cdn/photo-1504674900247-0877df9cc836.jpg', + }, + { + sense: 'Feel', + icon: Wind, + title: 'Warm Sand Between Your Toes', + description: 'Sink into powder-soft sand. Feel the warm breeze on your skin. Let every muscle relax.', + image: '/images/cdn/photo-1520454974749-611b7248ffdb.jpg', + }, +] + +const morningMoments = [ + { + time: '6:00 AM', + icon: Sunrise, + title: 'The Sunrise', + text: 'Step onto your private balcony. The sky is painted in impossible colors.', + }, + { + time: '7:30 AM', + icon: Coffee, + title: 'Coffee with a View', + text: 'Rich Mexican coffee, delivered to your terrace. The ocean stretches forever.', + }, + { + time: '8:30 AM', + icon: Utensils, + title: 'Breakfast Paradise', + text: 'Fresh tropical fruits, made-to-order omelets, pastries still warm from the oven.', + }, + { + time: '10:00 AM', + icon: TreePalm, + title: 'Your Day Begins', + text: 'Beach, pool, spa, adventure — the whole day is yours. No plans required.', + }, +] + +const dreamScenes = [ + { + title: 'Crystal Clear Waters', + image: '/images/cdn/photo-1510097467424-192d713fd8b2.jpg', + caption: 'Water so clear you can see the ocean floor', + }, + { + title: 'Sunset Cocktails', + image: '/images/cdn/photo-1581710862235-eb6e05d8783f.jpg', + caption: 'Every evening ends with a masterpiece sky', + }, + { + title: 'Infinity Pool', + image: '/images/cdn/photo-1540541338287-41700207dee6.jpg', + caption: 'Where the pool meets the horizon', + }, + { + title: 'Tropical Gardens', + image: '/images/cdn/photo-1512100356356-de1b84283e18.jpg', + caption: 'Lush, vibrant beauty at every turn', + }, +] + +const feelings = [ + { icon: Heart, text: 'Pure relaxation without guilt' }, + { icon: Sparkles, text: 'Wonder at nature\'s beauty' }, + { icon: Music, text: 'Joy in every moment' }, + { icon: Shell, text: 'Connection with someone special' }, + { icon: CloudSun, text: 'Freedom from daily stress' }, + { icon: Flower2, text: 'Peace you haven\'t felt in years' }, +] + +export default function LP38Sunrise() { + return ( +
+ + + {/* Hero Section */} +
+
+
+ Sunrise over Mexican beach +
+ +
+ + +

+ Imagine Waking Up +
+ + To This + +

+ +

+ Close your eyes. Feel the warm sand. Hear the waves. +
+ Smell the salt air. This is your morning in Mexico. +

+ +
+ $59/mo + ${PAYMENT_CONFIG.monthlyPrice}/mo + + SAVE 34% + +
+ +

+ 5 days & 4 nights all-inclusive. Just $1.30/day for paradise. +

+ + + Start My Paradise Morning + +
+
+ + + + {/* Sensory Section */} +
+
+

+ Experience Paradise With Every Sense +

+

+ This isn't just a vacation. It's a feeling. One you'll carry with you forever. +

+ +
+ {sensoryExperiences.map((exp, i) => ( +
+
+
+ {exp.title} +
+ {exp.sense} +
+
+
+
+
+ +
+

+ {exp.title} +

+

{exp.description}

+
+
+ ))} +
+
+
+ + {/* Morning Timeline */} +
+
+

+ Your Morning in Paradise +

+

+ Every sunrise is an invitation to fall in love with life again. +

+ +
+
+ + {morningMoments.map((moment, i) => ( +
+
+ +
+
+ {moment.time} +

+ {moment.title} +

+

{moment.text}

+
+
+ ))} +
+
+
+ + {/* Dream Gallery */} +
+
+

+ Scenes From Your Future Vacation +

+

+ Let yourself dream. These could be your photos in a few months. +

+ +
+ {dreamScenes.map((scene, i) => ( +
+ {scene.title} +
+
+

{scene.title}

+

{scene.caption}

+
+
+ ))} +
+
+
+ + {/* Feelings Grid */} +
+
+

+ What You'll Feel +

+

+ More than a trip. A transformation. +

+ +
+ {feelings.map((f, i) => ( +
+ +

{f.text}

+
+ ))} +
+
+
+ + {/* Pricing */} +
+
+

+ This Feeling Costs Less Than You Think +

+

+ $59/mo{' '} + ${PAYMENT_CONFIG.monthlyPrice}/mo or{' '} + ${PAYMENT_CONFIG.oneTimePrice} one-time +

+

+ That's just $1.30/day. Less than your morning coffee. +

+ +
+
+ + {/* Testimonials */} +
+
+

+ They Found Their Sunrise +

+
+ {TESTIMONIALS.slice(0, 3).map((t, i) => ( + + ))} +
+
+
+ + {/* Primary CTA - Ebook */} +
+
+ +

+ Start Planning Your Paradise Morning +

+

+ Download our free “Budget Luxury Travel” guide and start + imagining your first sunrise in Mexico. Don't wait — this feeling is closer than you think. +

+ + + +

+ Free instant download. Pure inspiration inside. +

+
+
+ + {/* Secondary CTA - Pay Now */} +
+
+

+ Don't Let This Feeling Fade +

+

+ You felt something reading this page. That's your heart telling you it's time. + Lock in this special price before it disappears. +

+

+ 30-day money-back guarantee. Zero risk. +

+ + +
+
+ + {/* Trust Badges */} +
+ +
+ + {/* FAQ */} + + +
+
+

+ Common Questions +

+ +
+
+ + {/* Final CTA */} +
+

+ Your Sunrise Is Waiting +

+

+ Stop dreaming. Start living. Paradise is only ${PAYMENT_CONFIG.monthlyPrice}/mo away. +

+ + Begin My Journey + +
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP39Adrenaline.tsx b/src/components/lp/pages/LP39Adrenaline.tsx new file mode 100644 index 0000000..1bb24a9 --- /dev/null +++ b/src/components/lp/pages/LP39Adrenaline.tsx @@ -0,0 +1,452 @@ +'use client' + +import { + Zap, Mountain, Waves, Wind, ArrowRight, Star, + Shield, Clock, Flame, Target, Trophy, Compass, + ChevronRight, Swords, Bike, Anchor, + Eye, Sparkles, Users, +} from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import TikTokCarousel from '@/components/lp/shared/TikTokCarousel' +import SocialProofTicker from '@/components/lp/shared/SocialProofTicker' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import ComparisonTable from '@/components/lp/shared/ComparisonTable' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const LIME = '#84CC16' +const BLACK = '#0A0A0A' +const ELECTRIC_BLUE = '#3B82F6' + +const adventures = [ + { + title: 'Zip-Lining', + description: 'Soar 200 feet above the jungle canopy at 45mph. Feel the wind rip past as the rainforest blurs below.', + image: '/images/cdn/photo-1530866495561-507c83010e82.jpg', + icon: Wind, + intensity: 'HIGH', + }, + { + title: 'Snorkeling', + description: 'Dive into crystal-clear cenotes. Swim alongside sea turtles and tropical fish in water so clear it feels unreal.', + image: '/images/cdn/photo-1544551763-46a013bb70d5.jpg', + icon: Waves, + intensity: 'MEDIUM', + }, + { + title: 'ATV Tours', + description: 'Tear through jungle trails and coastal paths on all-terrain vehicles. Mud, dust, and pure adrenaline.', + image: '/images/cdn/photo-1558618666-fcd25c85f82e.jpg', + icon: Bike, + intensity: 'HIGH', + }, + { + title: 'Cenote Diving', + description: 'Descend into ancient underground caves filled with impossibly blue water. Otherworldly and unforgettable.', + image: '/images/cdn/photo-1518638150340-f706e86654de.jpg', + icon: Compass, + intensity: 'EXTREME', + }, + { + title: 'Cliff Jumping', + description: 'Stand at the edge. Look down at turquoise water 30 feet below. Three... two... one... JUMP.', + image: '/images/cdn/photo-1581710862235-eb6e05d8783f.jpg', + icon: Mountain, + intensity: 'EXTREME', + }, + { + title: 'Parasailing', + description: 'Float 500 feet above the coastline. The entire Mexican Riviera stretches out below you like a painting.', + image: '/images/cdn/photo-1507525428034-b723cf961d3e.jpg', + icon: Anchor, + intensity: 'MEDIUM', + }, +] + +const dailyCost = [ + { item: 'A large coffee', cost: '$6.50' }, + { item: 'A fast food combo', cost: '$12.00' }, + { item: 'A movie ticket', cost: '$15.00' }, + { item: '5 DAYS IN MEXICO', cost: '$1.30/day', highlight: true }, +] + +const adventureStats = [ + { value: '6+', label: 'Adventure Activities', icon: Flame }, + { value: '4', label: 'Epic Destinations', icon: Target }, + { value: '5', label: 'Days of Adrenaline', icon: Trophy }, + { value: '∞', label: 'Food & Drinks', icon: Sparkles }, +] + +const intensityColors: Record = { + 'MEDIUM': ELECTRIC_BLUE, + 'HIGH': LIME, + 'EXTREME': '#EF4444', +} + +export default function LP39Adrenaline() { + return ( +
+ + + {/* Hero Section */} +
+
+ Adventure in Mexico +
+
+ +
+
+ + ADVENTURE AWAITS +
+ +

+ Mexico Isn't Just Beaches. +
+ It's{' '} + + ADVENTURE. + +

+ +

+ Zip-lining. Cenote diving. ATV tours. Cliff jumping. +
+ All this + unlimited food & drinks. +

+ +
+ $59/mo + ${PAYMENT_CONFIG.monthlyPrice}/mo + + SAVE 34% + +
+ + + Get the Adventure Guide + +
+
+ + + + {/* Adventure Stats */} +
+
+
+ {adventureStats.map((s, i) => ( +
+ +

+ {s.value} +

+

{s.label}

+
+ ))} +
+
+
+ + {/* Adventure Activities Grid */} +
+
+

+ Your Adrenaline Menu +

+

+ Six heart-pumping activities included with your all-inclusive certificate. +

+ +
+ {adventures.map((adv, i) => ( +
+
+ {adv.title} +
+
+ {adv.intensity} +
+
+
+
+
+ +
+

+ {adv.title} +

+
+

{adv.description}

+
+
+ ))} +
+
+
+ + {/* Cost Comparison */} +
+
+

+ All This For{' '} + ${PAYMENT_CONFIG.monthlyPrice}/mo +

+

+ That's $1.30/day for unlimited food, + drinks, resort access, AND adventure activities. Perspective check: +

+ +
+ {dailyCost.map((item, i) => ( +
+ + {item.item} + + + {item.cost} + +
+ ))} +
+ +
+ +
+
+
+ + {/* TikTok Section */} +
+
+

+ Watch The Action +

+

+ Real travelers. Real adventures. Real Mexico. +

+ +
+
+ + {/* Comparison Table */} +
+
+

+ Us vs. Everyone Else +

+

+ Adventure + all-inclusive at a price that doesn't exist anywhere else +

+ +
+
+ + {/* Testimonials */} +
+
+

+ Adrenaline Junkies Approve +

+
+ {TESTIMONIALS.slice(0, 3).map((t, i) => ( + + ))} +
+
+
+ + {/* Primary CTA - Ebook */} +
+
+ +

+ Get the Adventure Activities Guide +

+

+ Our free “Budget Luxury Travel” ebook includes a complete adventure + activities guide for all four destinations. Download it now — this price won't last. +

+ + + +

+ Free instant download. Fuel your adventure. +

+
+
+ + {/* Secondary CTA - Pay Now */} +
+
+

+ Lock In Your Adventure +

+

+ Every second you wait, someone else claims your spot. The price goes back to $59/mo when the timer hits zero. +

+

+ 30-day money-back guarantee. Zero risk. Maximum adrenaline. +

+ + +
+
+ + {/* Trust Badges */} +
+ +
+ + {/* FAQ */} + + +
+
+

+ Quick Answers +

+ +
+
+ + {/* Final CTA */} +
+
+ +

+ Your Adventure Starts Now +

+

+ Stop watching. Start doing. Mexico is calling. +

+ + Let's Go + +
+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP40GoldenTicket.tsx b/src/components/lp/pages/LP40GoldenTicket.tsx new file mode 100644 index 0000000..5aacad5 --- /dev/null +++ b/src/components/lp/pages/LP40GoldenTicket.tsx @@ -0,0 +1,519 @@ +'use client' + +import { useState, useEffect } from 'react' +import { + Ticket, Crown, Star, Sparkles, Gift, Lock, + ArrowRight, Shield, Award, Clock, Heart, + Gem, Trophy, Eye, Users, ChevronRight, + Zap, PartyPopper, BadgeCheck, +} from 'lucide-react' +import EbookCaptureForm from '@/components/lp/shared/EbookCaptureForm' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import TikTokCarousel from '@/components/lp/shared/TikTokCarousel' +import SocialProofTicker from '@/components/lp/shared/SocialProofTicker' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import PricingDisplay from '@/components/lp/shared/PricingDisplay' +import UrgencyBanner from '@/components/lp/shared/UrgencyBanner' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG, TESTIMONIALS, FAQ_ITEMS } from '@/app/lp/_config/types' + +const GOLD = '#F59E0B' +const RICH_BROWN = '#78350F' +const CREAM = '#FEF3C7' +const DARK_GOLD = '#B45309' + +const goldenPerks = [ + { + icon: Crown, + title: 'VIP Resort Access', + description: '5 days & 4 nights at a luxury all-inclusive resort. Food, drinks, activities — everything included.', + }, + { + icon: Gift, + title: 'Exclusive Pricing', + description: `Only $${PAYMENT_CONFIG.monthlyPrice}/mo (regular $59/mo). This golden ticket rate is not available anywhere else.`, + }, + { + icon: Gem, + title: '4 Premium Destinations', + description: 'Cancun, Cabo San Lucas, Riviera Maya, or Puerto Vallarta. Your choice.', + }, + { + icon: Star, + title: '18-Month Flexibility', + description: 'Book your paradise trip anytime within 18 months. No rush, no pressure.', + }, + { + icon: Shield, + title: '30-Day Guarantee', + description: 'Full refund within 30 days if you change your mind. Zero risk.', + }, + { + icon: Heart, + title: 'Bring a Guest', + description: 'Your golden ticket covers 2 adults + 2 kids under 12. The whole family flies to paradise.', + }, +] + +const scarcityMilestones = [ + { claimed: 38, total: 50, label: 'Golden Tickets Claimed' }, + { claimed: 847, total: 1000, label: 'Views Today' }, +] + +const exclusiveReasons = [ + 'This link was shared privately — it is not publicly available', + 'The golden ticket rate of $39/mo is 34% below standard pricing', + 'Only 50 golden tickets exist in this batch', + 'Each ticket is limited to one per household', + 'This page will expire when the countdown reaches zero', +] + +const ticketInclusions = [ + { item: '5 Days / 4 Nights', included: true }, + { item: 'All Meals & Drinks', included: true }, + { item: 'Resort Pools & Beach', included: true }, + { item: 'Entertainment & Activities', included: true }, + { item: 'Airport Shuttle', included: true }, + { item: '24/7 Concierge', included: true }, +] + +export default function LP40GoldenTicket() { + const [ticketsLeft, setTicketsLeft] = useState(12) + + useEffect(() => { + const interval = setInterval(() => { + setTicketsLeft((prev) => { + if (prev <= 3) return 3 + return Math.random() > 0.7 ? prev - 1 : prev + }) + }, 45000) + return () => clearInterval(interval) + }, []) + + return ( +
+ + + {/* Hero Section */} +
+
+ {/* Decorative golden particles */} +
+ {Array.from({ length: 20 }).map((_, i) => ( +
+ ))} +
+ +
+ {/* Golden Ticket Frame */} +
+
+ {/* Perforation edges */} +
+ {Array.from({ length: 8 }).map((_, i) => ( +
+ ))} +
+
+ {Array.from({ length: 8 }).map((_, i) => ( +
+ ))} +
+ + + +

+ You Found the +
+ Golden Ticket +

+ +
+ +

+ This exclusive link was shared with you personally. +
+ Only 50 golden tickets available. +

+ +
+ $59/mo + + ${PAYMENT_CONFIG.monthlyPrice}/mo + +
+ +

+ 5 days & 4 nights all-inclusive in Mexico +

+
+
+ + + Claim My Golden Ticket + +
+
+ + + + {/* Scarcity Counter */} +
+
+
+ +

+ Only {ticketsLeft} Golden Tickets Remaining +

+

+ 38 of 50 tickets have already been claimed. Once they're gone, this offer disappears forever. +

+ +
+
+
+
+

{50 - ticketsLeft} of 50 claimed

+
+
+
+
+ + {/* Why This Is Exclusive */} +
+
+

+ Why This Golden Ticket Is Special +

+

+ This isn't a regular promotion. Here's what makes it different. +

+ +
+ {exclusiveReasons.map((reason, i) => ( +
+
+ {i + 1} +
+

{reason}

+
+ ))} +
+
+
+ + {/* What Your Golden Ticket Includes */} +
+
+

+ Your Golden Ticket Includes +

+

+ Everything you need for the vacation of a lifetime. +

+ +
+ {goldenPerks.map((perk, i) => ( +
+
+ +
+

+ {perk.title} +

+

{perk.description}

+
+ ))} +
+
+
+ + {/* Ticket Checklist */} +
+
+
+
+

+ What's Included +

+ +
+ {ticketInclusions.map((item, i) => ( +
+ + {item.item} +
+ ))} +
+ +
+

Total value: $3,200+

+

+ ${PAYMENT_CONFIG.monthlyPrice}/mo × {PAYMENT_CONFIG.totalMonths} +

+

+ or ${PAYMENT_CONFIG.oneTimePrice} one-time +

+
+
+
+
+
+ + {/* Countdown */} +
+
+

+ This Golden Ticket Expires In +

+

+ When the timer reaches zero, this exclusive pricing disappears. + Don't lose your golden opportunity. +

+ + +
+
+ + {/* TikTok */} +
+
+

+ See What Awaits Golden Ticket Holders +

+

+ Real guests. Real resorts. Real paradise. +

+ +
+
+ + {/* Testimonials */} +
+
+

+ Previous Golden Ticket Winners +

+
+ {TESTIMONIALS.slice(0, 3).map((t, i) => ( + + ))} +
+
+
+ + {/* Primary CTA - Ebook */} +
+
+
+ +
+

+ Claim Your Golden Guide +

+

+ Download our free “Budget Luxury Travel” guide. See exactly what + your golden ticket unlocks. This exclusive guide is only available through golden ticket links. +

+ + + +

+ Free instant download. Exclusive golden ticket content. +

+
+
+ + {/* Secondary CTA - Pay Now */} +
+
+

+ Ready to Redeem Your Ticket? +

+

+ Only {ticketsLeft} golden tickets remain. When they're gone, the price returns to $59/mo. + This is your moment. +

+

+ 30-day money-back guarantee. Zero risk. Pure golden opportunity. +

+ + +
+
+ + {/* Trust Badges */} +
+ +
+ + {/* FAQ */} + + +
+
+

+ Golden Ticket FAQ +

+ +
+
+ + {/* Final CTA */} +
+
+
+ {[1, 2, 3, 4, 5].map((s) => ( + + ))} +
+

+ Don't Let Your Golden Ticket Expire +

+

+ {ticketsLeft} tickets left. This page disappears when they're gone. +

+ + Claim My Golden Ticket + +
+
+ + +
+ ) +} diff --git a/src/components/lp/pages/LP41SeatReserved.tsx b/src/components/lp/pages/LP41SeatReserved.tsx new file mode 100644 index 0000000..aa16b41 --- /dev/null +++ b/src/components/lp/pages/LP41SeatReserved.tsx @@ -0,0 +1,549 @@ +'use client' + +import { + CheckCircle2, Sparkles, Clock, Lock, ShieldCheck, + ArrowRight, Star, Gift, PlayCircle, Users, + Ticket, BookOpen, MapPin, PartyPopper, +} from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import { PAYMENT_CONFIG } from '@/app/lp/_config/types' + +const ORANGE = '#E8651A' +const DARK = '#0F172A' +const GOLD = '#F59E0B' +const CREAM = '#FEF7ED' + +const transformations = [ + { + name: 'Todd & Jessica Dudley', + location: 'Chicago, IL', + photo: '/images/cdn/photo-1522529599102-193c0d76b5b6.jpg', + before: 'We\u2019d been pricing Cancun for months. Every site wanted $2,800+ per couple.', + moment: 'When I saw the $29/mo certificate I almost scrolled past \u2014 I thought it had to be fake.', + after: 'We booked Riviera Maya, flew out 5 weeks later. Oceanfront suite, unlimited everything. 95% of the puzzle we were missing was just this deal.', + }, + { + name: 'Octavia Taylor', + location: 'Atlanta, GA', + photo: '/images/cdn/photo-1494790108377-be9c29b29330.jpg', + before: 'I kept putting off our anniversary trip \u2014 couldn\u2019t justify spending $3K.', + moment: 'A friend sent me the link. I locked in the $249 one-time option in under 10 minutes.', + after: 'Cabo San Lucas. Sunset catamaran, swim-up bar, the works. The only tweak I needed was letting myself book it.', + }, + { + name: 'Debbie Dussler', + location: 'Phoenix, AZ', + photo: '/images/cdn/photo-1438761681033-6461ffad8d80.jpg', + before: 'My husband thought it was a scam \u2014 we almost cancelled after signing up.', + moment: 'Customer service walked us through everything. We kept the certificate and picked Puerto Vallarta.', + after: 'Best 5 days we\u2019ve had in a decade. We\u2019re already planning year two at Cancun.', + }, +] + +const benefits = [ + { + icon: Ticket, + title: 'Your All-Inclusive Certificate', + desc: '5 days / 4 nights for two at a 4-5\u2605 Mexico resort \u2014 meals, drinks, pools, beaches included.', + }, + { + icon: MapPin, + title: '4 Destinations to Choose From', + desc: 'Cancun, Cabo San Lucas, Riviera Maya, or Puerto Vallarta \u2014 book whichever fits your mood.', + }, + { + icon: BookOpen, + title: 'The Paradise Planning Vault', + desc: 'Our 11-page insider guide: packing lists, excursion picks, flight hacks, what to skip.', + }, + { + icon: PartyPopper, + title: 'Bring a Guest \u2014 Free', + desc: 'Family of 4 — 2 adults + 2 kids under 12, all covered at no extra charge.', + }, +] + +const socialProof = [ + { + quote: 'Booked Cancun for our 10th anniversary. Ocean view suite, infinity pool, and we paid less than one night would cost elsewhere.', + name: 'Sarah & Mike', + location: 'Chicago, IL', + photo: '/images/cdn/photo-1522529599102-193c0d76b5b6.jpg', + }, + { + quote: 'I\u2019m a travel agent. This is the single best certificate I\u2019ve seen in 14 years of booking Mexico trips.', + name: 'Rachel T.', + location: 'Phoenix, AZ', + photo: '/images/cdn/photo-1544005313-94ddf0286df2.jpg', + }, + { + quote: 'Split it into $29/mo payments and barely felt it. Riviera Maya was unreal \u2014 cenote tour, swim-up bars, all included.', + name: 'James & Patricia', + location: 'Miami, FL', + photo: '/images/cdn/photo-1500648767791-00dcc994a43e.jpg', + }, + { + quote: 'My husband thought it was a scam. I booked anyway. He spent the whole flight home apologizing.', + name: 'Maria G.', + location: 'Houston, TX', + photo: '/images/cdn/photo-1438761681033-6461ffad8d80.jpg', + }, + { + quote: 'We paid $249 total. For 5 days all-inclusive. I\u2019ve done the math three times \u2014 it still works out.', + name: 'David & Lisa', + location: 'Denver, CO', + photo: '/images/cdn/photo-1472099645785-5658abf4ff4e.jpg', + }, +] + +export default function LP41SeatReserved() { + const scrollToForm = () => { + document.getElementById('signup-form')?.scrollIntoView({ behavior: 'smooth' }) + } + + return ( +
+ + + {/* ===== HEADER CONFIRMATION ===== */} +
+
+
+ + + Your Spot Is Being Held + +
+ +

+ Your Paradise Seat Is +
Reserved — For the Next 30 Minutes +

+

+ Where guesswork ends and a real Mexico vacation begins. Private resort access, lifetime booking window, and + the insider documents no travel agent will send you. +

+ +
+ + This price expires in + +
+ +
+ +

or ${PAYMENT_CONFIG.oneTimePrice} one-time · 100% refund within 30 days

+
+
+
+ + {/* ===== TRANSFORMATION STORIES ===== */} +
+
+
+ Real Travelers +

+ The Missing Piece Most Couples Never Find +

+

+ Not information. Not another listicle. A real certificate, redeemable at real resorts, for a price + that stops making sense the moment you see it. +

+
+ +
+ {transformations.map((t, i) => ( +
+
+
+ {t.name} +
+

{t.name}

+

{t.location}

+
+ {[...Array(5)].map((_, j) => )} +
+
+
+
+
+

Before

+

{t.before}

+
+
+

The Moment

+

“{t.moment}”

+
+
+

After

+

{t.after}

+
+
+
+
+ ))} +
+
+
+ + {/* ===== CORE BENEFITS ===== */} +
+
+
+ What's Included +

+ A Certificate — Not Just Another Travel Membership +

+

+ Four things stack together to make this work. Take any one away and the math breaks. +

+
+ +
+ {benefits.map((b, i) => ( +
+
+
+ +
+
+

{b.title}

+

{b.desc}

+
+
+
+ ))} +
+ +
+
+ + Lock-in price expires in + +
+
+
+
+ + {/* ===== PRICING COMPARISON ===== */} +
+
+
+

+ Pick Your Paradise Tier +

+

+ Both options include the same resort. The difference is how you pay — and what you save. +

+
+ +
+ {/* Monthly */} +
+
+ Payment Plan + POPULAR +
+
+

$59/mo

+

+ ${PAYMENT_CONFIG.monthlyPrice}/mo +

+

× {PAYMENT_CONFIG.totalMonths} months · ${PAYMENT_CONFIG.totalPrice} total

+
+
    + {[ + '5 days / 4 nights all-inclusive', + 'Choose from 4 Mexico destinations', + 'Bring a guest at no extra cost', + '18 months flexible booking', + 'Book immediately after first payment', + 'Paradise Planning Vault ebook', + ].map((f, i) => ( +
  • + + {f} +
  • + ))} +
+ +
+ + {/* One-time */} +
+
+ + BEST VALUE — SAVE ${PAYMENT_CONFIG.totalPrice - PAYMENT_CONFIG.oneTimePrice} + +
+
+ Pay Once & Done + +
+
+

$599

+

+ ${PAYMENT_CONFIG.oneTimePrice} +

+

+ Save ${PAYMENT_CONFIG.totalPrice - PAYMENT_CONFIG.oneTimePrice} vs. monthly plan +

+
+
    + {[ + 'Everything in the monthly plan', + 'No recurring charges, ever', + 'Priority resort assignment', + 'Extra guest upgrade voucher', + 'Concierge booking hotline', + 'VIP check-in at resort front desk', + ].map((f, i) => ( +
  • + + {f} +
  • + ))} +
+ +
+
+ +

+ Both options include the full 30-day money-back guarantee. Cancel the monthly plan anytime. +

+
+
+ + {/* ===== SOCIAL PROOF ===== */} +
+
+
+
+ + Over 2,847 certificates claimed this month +
+

+ Real Travelers. Real Resorts. Real Receipts. +

+
+ +
+ {socialProof.slice(0, 3).map((t, i) => )} +
+
+ {socialProof.slice(3).map((t, i) => )} +
+
+
+ + {/* ===== MESSAGING REINFORCEMENT ===== */} +
+
+ +

+ The Booking Window Is Closing +

+

+ Resort allotments are released weekly. The current batch expires at midnight, and the same certificate will + reprice at $59/mo for the next cohort. +

+

+ Later almost always turns into never. Lock it in while the discount is still on the table. +

+ +
+
+ + {/* ===== RECAP ===== */} +
+
+

+ Here's Everything You Get Today +

+
+
    + {[ + { label: 'All-Inclusive Mexico Certificate (5D/4N)', value: '$1,499' }, + { label: 'Bring-a-Guest Upgrade (included)', value: '$699' }, + { label: 'Paradise Planning Vault (11-page ebook)', value: '$97' }, + { label: 'Concierge Booking Hotline', value: '$149' }, + { label: '18-Month Flexible Booking Window', value: '$199' }, + { label: '30-Day Money-Back Guarantee', value: 'Priceless' }, + ].map((row, i) => ( +
  • + + + {row.label} + + {row.value} +
  • + ))} +
+
+ Total real-world value + $2,643+ +
+
+ Your price today + + ${PAYMENT_CONFIG.monthlyPrice}/mo + +
+
+
+
+ + {/* ===== ORDER FORM ===== */} +
+
+
+ {/* Top banner with countdown */} +
+
+ + Price locks in + +
+
+ +
+
+ + Step 1 of 2 +
+

+ Reserve Your Discounted Seat +

+

+ Starts at ${PAYMENT_CONFIG.monthlyPrice}/mo · the + price rises after this countdown hits zero. +

+ + + +
+
+ + 100% Money-Back Within 30 Days +
+

+ If the resort doesn't meet your expectations, we refund every penny. No hoops, no hard feelings. +

+
+ +
+ +
+
+
+ +

+ 256-bit SSL · PCI compliant · Cards charged via NMI +

+
+
+ + + + {/* ===== FAQ ===== */} +
+
+

+ Questions Before You Reserve? +

+ +
+
+ + {/* ===== FINAL CTA ===== */} +
+
+ +

+ The Discount Closes When the Clock Hits Zero +

+

+ Same resort. Same certificate. Same 5 days of unlimited everything. Just not at this price. +

+ +

+ Starts at ${PAYMENT_CONFIG.monthlyPrice}/mo · 30-day money-back · Cancel anytime +

+
+
+ + {/* ===== COMPLIANCE FOOTER ===== */} +
+
+

Earnings & Travel Disclaimer

+

+ Travel certificates are subject to availability, blackout dates, and resort terms. Results shown reflect + individual traveler experiences and are not guaranteed. You are solely responsible for any flights, + transfers, and tips not covered by the all-inclusive package. +

+

+ Mexico Paradise Vacations has been connecting travelers to certified Mexico resorts since 2008. For + questions, call (888) 602-2424 or email{' '} + support@724vacation.com. +

+

+ © {new Date().getFullYear()} Mexico Paradise Vacations ·{' '} + Privacy Policy ·{' '} + Terms +

+
+
+
+ ) +} diff --git a/src/components/lp/pages/LP42VIPPass.tsx b/src/components/lp/pages/LP42VIPPass.tsx new file mode 100644 index 0000000..f6bb285 --- /dev/null +++ b/src/components/lp/pages/LP42VIPPass.tsx @@ -0,0 +1,529 @@ +'use client' + +import { + Crown, Lock, Clock, CheckCircle2, XCircle, ArrowRight, + Gem, Gift, Sparkles, ShieldCheck, Star, Ticket, + PhoneCall, BookOpen, Plane, Users, MapPin, +} from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import TestimonialCard from '@/components/lp/shared/TestimonialCard' +import InfluencerBuzz from '@/components/lp/shared/InfluencerBuzz' +import { PAYMENT_CONFIG } from '@/app/lp/_config/types' + +const PURPLE = '#6D28D9' +const DEEP = '#1E1B4B' +const GOLD = '#F59E0B' +const GOLD_DEEP = '#B45309' + +const vipBonuses = [ + { + icon: PhoneCall, + title: 'Private Concierge Line', + desc: 'Direct access to our Mexico booking desk. No call centers, no waits.', + value: '$149', + }, + { + icon: BookOpen, + title: 'Paradise Planning Vault', + desc: '11-page insider guide: packing, excursions, flight hacks, what to skip.', + value: '$97', + }, + { + icon: Plane, + title: 'Free Guest Upgrade Voucher', + desc: 'Bring a third traveler at 50% off \u2014 only available to VIP holders.', + value: '$299', + }, + { + icon: Sparkles, + title: 'VIP Check-in at Resort', + desc: 'Skip the front-desk line, welcome drink, early room assignment.', + value: '$129', + }, + { + icon: Gift, + title: 'Lifetime Rebooking Rights', + desc: 'If dates fall through, re-use your certificate forever. No expiration.', + value: '$199', + }, +] + +const vipTestimonials = [ + { + quote: "The concierge line alone paid for the VIP. She upgraded us to oceanfront for free and booked our catamaran sunset cruise.", + name: "Jennifer & Tom", + location: "New York, NY", + photo: "/images/cdn/photo-1494790108377-be9c29b29330.jpg", + }, + { + quote: "VIP check-in was unreal. We walked past a 40-minute line, got welcome margaritas, and our suite was ready at 11am.", + name: "David & Lisa", + location: "Denver, CO", + photo: "/images/cdn/photo-1472099645785-5658abf4ff4e.jpg", + }, + { + quote: "My sister joined us last-minute. The VIP guest voucher saved us $600. Cabo was a complete dream.", + name: "Maria G.", + location: "Houston, TX", + photo: "/images/cdn/photo-1438761681033-6461ffad8d80.jpg", + }, +] + +const standardFeatures = [ + { label: '5 days / 4 nights all-inclusive', included: true }, + { label: 'Choose 1 of 4 Mexico destinations', included: true }, + { label: 'Bring a guest at no extra cost', included: true }, + { label: '18-month flexible booking window', included: true }, + { label: 'Paradise Planning Vault ebook', included: false }, + { label: 'Private concierge booking line', included: false }, + { label: 'VIP resort check-in privileges', included: false }, + { label: 'Third-guest upgrade voucher', included: false }, + { label: 'Lifetime rebooking rights', included: false }, +] + +const vipFeatures = [ + { label: '5 days / 4 nights all-inclusive', included: true }, + { label: 'Choose 1 of 4 Mexico destinations', included: true }, + { label: 'Bring a guest at no extra cost', included: true }, + { label: '18-month flexible booking window', included: true }, + { label: 'Paradise Planning Vault ebook', included: true }, + { label: 'Private concierge booking line', included: true }, + { label: 'VIP resort check-in privileges', included: true }, + { label: 'Third-guest upgrade voucher', included: true }, + { label: 'Lifetime rebooking rights', included: true }, +] + +export default function LP42VIPPass() { + const scrollToForm = () => { + document.getElementById('signup-form')?.scrollIntoView({ behavior: 'smooth' }) + } + + return ( +
+ + + {/* ===== HERO ===== */} +
+
+
+ + + Invitation-Only VIP Access + +
+ +

+ Upgrade Your Certificate to +
{' '} + VIP Platinum +

+

+ Private concierge, lifetime rebooking, guest upgrades, and skip-the-line check-in. + Only the first 100 seats each month unlock at this price. +

+ +
+ + VIP pricing locks in for + +
+ +
+ +

+ or ${PAYMENT_CONFIG.oneTimePrice} one-time · 100% money-back for 30 days +

+
+
+
+ + {/* ===== BONUS STACK ===== */} +
+
+
+ The VIP Stack +

+ 5 Bonuses That Come Free With VIP +

+

+ None of these are sold separately. They only unlock when you upgrade today. +

+
+ +
+ {vipBonuses.map((b, i) => ( +
+
+ +
+
+

{b.title}

+

{b.desc}

+
+
+

Value

+

{b.value}

+
+
+ ))} +
+ +
+
+

Total bonus value

+

$873

+
+
+
+

Your price

+

FREE with VIP

+
+ +
+
+
+ + {/* ===== TIER COMPARISON ===== */} +
+
+
+

+ VIP Basic vs. VIP Platinum +

+

+ Same resort. Wildly different experience. +

+
+ +
+ {/* Basic tier */} +
+
+
+ + VIP Basic +
+ AVAILABLE +
+
+

$299

+

+ ${PAYMENT_CONFIG.monthlyPrice}/mo +

+

for 10 months, total ${PAYMENT_CONFIG.totalPrice}

+
+
    + {standardFeatures.map((f, i) => ( +
  • + {f.included ? ( + + ) : ( + + )} + {f.label} +
  • + ))} +
+ +
+ + {/* VIP Platinum tier */} +
+
+ +
+
+
+ + + VIP Platinum + +
+ + RECOMMENDED + +
+
+

$599

+

+ ${PAYMENT_CONFIG.oneTimePrice}/one-time +

+

+ Save ${PAYMENT_CONFIG.totalPrice - PAYMENT_CONFIG.oneTimePrice} vs. monthly · No recurring charges +

+
+
    + {vipFeatures.map((f, i) => ( +
  • + + {f.label} +
  • + ))} +
+ +

+ Only 100 seats released monthly · this cohort expires at midnight +

+
+
+
+ + {/* Sold Out tier callout */} +
+
+
+ +
+
+
+

VIP Black Tier

+ + SOLD OUT + +
+

+ Private villa, personal butler, $997 tier — all 12 seats claimed for this cycle. Waitlist opens next month. +

+
+ +
+
+
+
+ + {/* ===== VIP TESTIMONIALS ===== */} +
+
+
+
+ + Verified VIP travelers +
+

+ Why They Upgraded — And Would Again +

+
+ +
+ {vipTestimonials.map((t, i) => ( +
+
+ + VIP Platinum + +
+ +
+ ))} +
+
+
+ + {/* ===== DESTINATIONS STRIP ===== */} +
+
+
+

Redeemable At 4 Flagship Resorts

+

VIP members get priority on all four locations, even during peak weeks.

+
+
+ {[ + { name: 'Cancun', img: '/images/cdn/photo-1510097467424-192d713fd8b2.jpg' }, + { name: 'Cabo San Lucas', img: '/images/cdn/photo-1593655600619-a88c11180241.jpg' }, + { name: 'Riviera Maya', img: '/images/cdn/photo-1581710862235-eb6e05d8783f.jpg' }, + { name: 'Puerto Vallarta', img: '/images/cdn/photo-1585793753011-397e6e4668d6.jpg' }, + ].map((d, i) => ( +
+ {d.name} +
+
+ + {d.name} +
+
+ ))} +
+
+
+ + {/* ===== GUARANTEE ===== */} +
+
+
+
+
+
+ +
+
+
+

+ The 30-Day VIP Guarantee +

+

+ Claim VIP today. If you don't feel it's worth every penny within 30 days, we refund you in full + and you keep the Paradise Planning Vault as a parting gift. No hoops. No phone tag. +

+
+
+
+
+
+ + {/* ===== ORDER FORM ===== */} +
+
+
+ {/* Top banner with countdown */} +
+
+ + VIP cohort closes in + +
+
+ +
+
+ + VIP Platinum Enrollment +
+

+ Lock In Your VIP Access +

+

+ Starts at ${PAYMENT_CONFIG.monthlyPrice}/mo or{' '} + ${PAYMENT_CONFIG.oneTimePrice} once. All 5 bonuses unlock immediately. +

+ + + +
+
+ + 30-Day Full Refund Guarantee +
+

+ Don't love it? Email us for a 100% refund — keep the Planning Vault as our gift. +

+
+ +
+ +
+
+
+ +
+ 256-bit SSL + 2,847+ VIP members + PCI compliant +
+
+
+ + + + {/* ===== FINAL CTA ===== */} +
+
+ + +

+ VIP Access Closes at Midnight +

+

+ After this cohort fills, the next VIP enrollment reprices to $299/mo. Same resort, same trip, + different price. +

+ +

+ Starts at ${PAYMENT_CONFIG.monthlyPrice}/mo · 30-day money-back · Cancel anytime +

+
+
+ + {/* ===== COMPLIANCE FOOTER ===== */} +
+
+

Earnings & Travel Disclaimer

+

+ VIP bonuses are delivered digitally and at the resort front desk upon arrival. Certificates are subject to + availability, blackout dates, and resort terms. Individual traveler experiences vary. VIP tier caps reset + the 1st of each month. +

+

+ Mexico Paradise Vacations has connected travelers to certified Mexico resorts since 2008. Support line:{' '} + (888) 602-2424. Email:{' '} + support@724vacation.com. +

+

+ © {new Date().getFullYear()} Mexico Paradise Vacations ·{' '} + Privacy Policy ·{' '} + Terms +

+
+
+
+ ) +} diff --git a/src/components/lp/pages/LP43RealTraveler.tsx b/src/components/lp/pages/LP43RealTraveler.tsx new file mode 100644 index 0000000..5b24fee --- /dev/null +++ b/src/components/lp/pages/LP43RealTraveler.tsx @@ -0,0 +1,304 @@ +'use client' + +import { + CheckCircle, Sparkles, Clock, Lock, ShieldCheck, Star, Users, + Sun, Heart, Waves, Moon, Sandwich, Wind, Coffee, Eye, Smile, + Plane, MapPin, Phone, X, +} from 'lucide-react' +import PayNowForm from '@/components/lp/shared/PayNowForm' +import CountdownTimer from '@/components/lp/shared/CountdownTimer' +import FAQAccordion from '@/components/lp/shared/FAQAccordion' +import TrustBadges from '@/components/lp/shared/TrustBadges' +import StickyMobileCTA from '@/components/lp/shared/StickyMobileCTA' +import { PAYMENT_CONFIG, TESTIMONIALS } from '@/app/lp/_config/types' + +const SLUG = 'real-traveler' +const ORANGE = '#E8651A' +const SUPPORT_PHONE = '888-602-2424' + +export default function LP43RealTraveler() { + return ( +
+ + {/* Top trust strip */} +
+
+
+ + Secure 256-bit SSL · 30-Day Money-Back Guarantee +
+ + + {SUPPORT_PHONE} + +
+
+ + {/* ───── 1. HEADLINE — bold promise ───── */} +
+
+
+ + Real traveler · Day 4 in Mexico +
+

+ She Paid $290 Total
+ For This 5-Star Mexico Vacation +

+

+ Watch her 20-second story below. Same resort, same dates, same 5 days — + last year cost her $2,800. +

+
+ + {/* ───── HERO VIDEO — the testimonial ───── */} +
+
+ +
+ + Real traveler +
+
+

+ Filmed on her iPhone · No script · No edit +

+
+
+ + {/* ───── 2. LEAD — set the stage ───── */} +
+

+ You've been pricing Cancun all week. Every site quotes $2,400, $3,200, $3,800. + You close the tab, promise yourself “next year.” +

+

+ Here's what nobody told you: the resorts are the same. You've just been buying from the wrong place. +

+
+ + {/* ───── 3. STORY — the OLD way ───── */} +
+
+
+

The Old Way

+

Why your last vacation cost $3,000+

+
+
+ {[ + { x: 'Booking the day-of through Expedia or hotel direct', why: 'You pay retail. Retail is brutal.' }, + { x: 'No volume discounts because you book once a year', why: 'Resorts price-discriminate by frequency.' }, + { x: 'Rigid date windows: "We need it for spring break"', why: 'Peak dates = peak prices, every time.' }, + { x: '"Resort fees" + drinks + tips not in the upfront price', why: '$300 a day adds up before you even unpack.' }, + ].map((row, i) => ( +
+

+ {row.x} +

+

{row.why}

+
+ ))} +
+
+
+ + {/* ───── 4. PITCH — the NEW way ───── */} +
+
+
+

The New Way

+

A pre-paid certificate that locks in resort prices

+

+ You pay $29 a month for 10 months (or $249 one-time). + That gets you a certificate for 5 days / 4 nights all-inclusive at any of 4 luxury Mexican resorts — + redeemable any time within 18 months. +

+
+ +
+ {[ + { i: , t: '5 days / 4 nights', d: 'All-inclusive luxury resort' }, + { i: , t: 'Whole family covered', d: '2 adults + 2 kids under 12' }, + { i: , t: 'Unlimited everything', d: 'Food, drinks, premium liquor' }, + { i: , t: '4 destinations', d: 'Cancun · Cabo · Riviera Maya · Puerto Vallarta' }, + { i: , t: '18-month window', d: 'Use whenever your calendar works' }, + { i: , t: '30-day refund', d: 'Full money-back guarantee' }, + ].map((b, i) => ( +
+
{b.i}
+
+

{b.t}

+

{b.d}

+
+
+ ))} +
+
+
+ + {/* ───── Body + Mind reset ───── */} +
+
+
+

+ It's not just a vacation.
+ It's a reset for your body and mind. +

+
+
+
+
+
+

For your body

+
+
    +
  • 5 days of real sunshine resets your circadian rhythm
  • +
  • Ocean swims that don't feel like exercise
  • +
  • Fresh grilled fish, fruit, real meals — inflammation drops
  • +
  • Sleep without alarms. No Sunday-night dread.
  • +
+
+
+
+
+

For your mind

+
+
    +
  • 5 days off the grid. Work email locked in a vault.
  • +
  • Looking at the horizon for 5 days makes problems look small
  • +
  • No phones at dinner. Actually hear who you love.
  • +
  • Memories you'll replay on your worst Tuesdays
  • +
+
+
+
+
+ + {/* ───── 5. EVIDENCE — testimonials + stars + ticker ───── */} +
+
+
+
+ {[1,2,3,4,5].map(i => )} + 4.9 from 2,847 reviews +
+

She's not the only one

+

Real travelers. Real photos. Real trips.

+
+
+ {TESTIMONIALS.slice(0, 3).map((t, i) => ( +
+
+ {[1,2,3,4,5].map(j => )} +
+

“{t.quote}”

+
+ {t.photo && {t.name}} +
+

{t.name}

+

{t.location}

+
+
+
+ ))} +
+
+
+ + {/* ───── 6. OFFER + 7. CLOSE — pricing + CTA + urgency ───── */} +
+
+
+
+ + Founders pricing — locks soon +
+

+ Lock in $29/mo before this rate ends. +

+
+ +
+
+ +
+ {/* Pricing anchor */} +
+

Regular: $39/mo

+

+ ${PAYMENT_CONFIG.monthlyPrice}/mo × {PAYMENT_CONFIG.totalMonths} +

+

+ Save ${(39 - PAYMENT_CONFIG.monthlyPrice) * PAYMENT_CONFIG.totalMonths} vs regular · or one-time ${PAYMENT_CONFIG.oneTimePrice} +

+
+ + + + {/* Risk reversal stack */} +
+
+ +
+

30-day money-back, no questions

+

If you're not 100% satisfied, every penny back. Just email.

+
+
+
+ +
+

Questions? Talk to a human

+

+ Call {SUPPORT_PHONE} · Mon-Sun 8am-10pm CT +

+
+
+
+
+ + +
+
+ + {/* ───── Honest truth callout (Sam-style close) ───── */} +
+
+

The honest truth

+

+ You're not really buying a vacation.
+ You're buying the version of yourself that comes back from it. +

+

+ Calmer. Lighter. With photos of people you love laughing on a beach.
+ For less than what most people spend on Saturday-night dinners in a month. +

+ + Claim My Certificate + +

+ 30-day money-back · No long-term commitment · Cancel anytime +

+
+
+ + {/* ───── FAQ ───── */} +
+
+

Quick answers

+ +
+
+ + +
+ ) +} diff --git a/src/components/lp/shared/ComparisonTable.tsx b/src/components/lp/shared/ComparisonTable.tsx new file mode 100644 index 0000000..b54c1d7 --- /dev/null +++ b/src/components/lp/shared/ComparisonTable.tsx @@ -0,0 +1,68 @@ +'use client' + +import { Check, X } from 'lucide-react' +import { PAYMENT_CONFIG } from '@/app/lp/_config/types' + +interface ComparisonTableProps { + className?: string + accentColor?: string + variant?: 'default' | 'dark' +} + +export default function ComparisonTable({ + className = '', + accentColor = '#4CAF50', + variant = 'default', +}: ComparisonTableProps) { + const isDark = variant === 'dark' + + const features = [ + { feature: '5 Days / 4 Nights', us: true, expedia: true, direct: true }, + { feature: 'All-Inclusive (meals & drinks)', us: true, expedia: false, direct: false }, + { feature: 'Resort Amenities', us: true, expedia: true, direct: true }, + { feature: 'Flexible Dates', us: true, expedia: true, direct: true }, + { feature: 'Payment Plan Available', us: true, expedia: false, direct: false }, + { feature: 'Price Guarantee', us: true, expedia: false, direct: false }, + ] + + return ( +
+ + + + + + + + + + + {features.map((row) => ( + + + + + + + ))} + + + + + + + +
Feature + Mexico Paradise + ExpediaDirect Booking
{row.feature} + {row.us ? : } + + {row.expedia ? : } + + {row.direct ? : } +
Total Price + ${PAYMENT_CONFIG.oneTimePrice} + $2,500+$3,000+
+
+ ) +} diff --git a/src/components/lp/shared/CountdownTimer.tsx b/src/components/lp/shared/CountdownTimer.tsx new file mode 100644 index 0000000..c962e1e --- /dev/null +++ b/src/components/lp/shared/CountdownTimer.tsx @@ -0,0 +1,110 @@ +'use client' + +import { useState, useEffect } from 'react' + +interface CountdownTimerProps { + minutes?: number + variant?: 'flip' | 'digital' | 'minimal' | 'banner' + className?: string + textColor?: string + bgColor?: string + accentColor?: string + message?: string +} + +export default function CountdownTimer({ + minutes = 30, + variant = 'digital', + className = '', + textColor = '#FFFFFF', + bgColor = '#000000', + accentColor = '#F44336', + message, +}: CountdownTimerProps) { + const [timeLeft, setTimeLeft] = useState(() => { + // Session-based 30-min countdown using sessionStorage + if (typeof window !== 'undefined') { + const stored = sessionStorage.getItem('lp_countdown_end') + if (stored) { + const diff = parseInt(stored) - Date.now() + if (diff > 0) return diff + } + const end = Date.now() + minutes * 60 * 1000 + sessionStorage.setItem('lp_countdown_end', end.toString()) + return minutes * 60 * 1000 + } + return minutes * 60 * 1000 + }) + + useEffect(() => { + const timer = setInterval(() => { + setTimeLeft(prev => Math.max(0, prev - 1000)) + }, 1000) + return () => clearInterval(timer) + }, []) + + const totalSeconds = Math.floor(timeLeft / 1000) + const m = Math.floor(totalSeconds / 60) + const s = totalSeconds % 60 + + const pad = (n: number) => n.toString().padStart(2, '0') + + const isUrgent = totalSeconds < 300 // Last 5 minutes + + if (variant === 'banner') { + return ( +
+

+ {message || '⚠️ LIMITED TIME — This promotional price expires in'}{' '} + + {pad(m)}:{pad(s)} + + {' '}— Act now or lose this deal forever +

+
+ ) + } + + if (variant === 'flip') { + const FlipUnit = ({ value, label }: { value: string; label: string }) => ( +
+
+
+ {value} +
+ {label} +
+ ) + + return ( +
+ + : + +
+ ) + } + + if (variant === 'minimal') { + return ( + + {pad(m)}:{pad(s)} + + ) + } + + // Digital variant + return ( +
+ {pad(m)} + : + {pad(s)} +
+ ) +} diff --git a/src/components/lp/shared/DestinationCarousel.tsx b/src/components/lp/shared/DestinationCarousel.tsx new file mode 100644 index 0000000..acc7c97 --- /dev/null +++ b/src/components/lp/shared/DestinationCarousel.tsx @@ -0,0 +1,85 @@ +'use client' + +import useEmblaCarousel from 'embla-carousel-react' +import { useCallback, useEffect, useState } from 'react' +import { ChevronLeft, ChevronRight } from 'lucide-react' +import { DESTINATIONS } from '@/app/lp/_config/types' + +interface DestinationCarouselProps { + className?: string + variant?: 'default' | 'dark' | 'compact' +} + +export default function DestinationCarousel({ + className = '', + variant = 'default', +}: DestinationCarouselProps) { + const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true }) + const [selectedIndex, setSelectedIndex] = useState(0) + + const scrollPrev = useCallback(() => emblaApi?.scrollPrev(), [emblaApi]) + const scrollNext = useCallback(() => emblaApi?.scrollNext(), [emblaApi]) + + useEffect(() => { + if (!emblaApi) return + const onSelect = () => setSelectedIndex(emblaApi.selectedScrollSnap()) + emblaApi.on('select', onSelect) + return () => { emblaApi.off('select', onSelect) } + }, [emblaApi]) + + const isDark = variant === 'dark' + + return ( +
+
+
+ {DESTINATIONS.map((dest) => ( +
+
+ {dest.name} +
+
+

{dest.name}

+

{dest.tagline}

+
+
+
+ ))} +
+
+ + + + +
+ {DESTINATIONS.map((_, i) => ( +
+
+ ) +} diff --git a/src/components/lp/shared/DirectCheckout.tsx b/src/components/lp/shared/DirectCheckout.tsx new file mode 100644 index 0000000..6e55a14 --- /dev/null +++ b/src/components/lp/shared/DirectCheckout.tsx @@ -0,0 +1,295 @@ +'use client' + +// Right-rail funnel, 3 states: +// survey — qualify (income $70k+? couple?) + capture email/phone (saved +// immediately, NO double opt-in). Filters out people who'd be +// rejected at the resort presentation. +// checkout — qualified only: minimal card + ZIP (name derived server-side). +// declined — not qualified: captured + soft decline, no hard sell. + +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { + Loader2, Lock, CreditCard, CheckCircle, ShieldCheck, Star, Undo2, ArrowRight, X, ChevronRight, Phone, +} from 'lucide-react' +import { toast } from 'sonner' +import { PAYMENT_CONFIG } from '@/app/lp/_config/types' +import { useTrackingParams } from '@/hooks/useTrackingParams' + +const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + +interface DirectCheckoutProps { + accentColor?: string + sourceLp?: string + className?: string +} + +type YN = '' | 'yes' | 'no' + +export default function DirectCheckout({ + accentColor = '#E8651A', + sourceLp = 'home', + className = '', +}: DirectCheckoutProps) { + const tracking = useTrackingParams(sourceLp) + const [step, setStep] = useState<'survey' | 'checkout' | 'declined'>('survey') + const [income, setIncome] = useState('') + const [couple, setCouple] = useState('') + const [email, setEmail] = useState('') + const [phone, setPhone] = useState('') + const [cardNumber, setCardNumber] = useState('') + const [cardExp, setCardExp] = useState('') + const [cardCvv, setCardCvv] = useState('') + const [zip, setZip] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [isSuccess, setIsSuccess] = useState(false) + const [paymentType, setPaymentType] = useState<'monthly' | 'one-time'>('monthly') + const [paymentError, setPaymentError] = useState(null) + + const amount = paymentType === 'monthly' ? PAYMENT_CONFIG.monthlyPrice : PAYMENT_CONFIG.oneTimePrice + + const formatCardNumber = (v: string) => + v.replace(/\D/g, '').slice(0, 16).replace(/(\d{4})(?=\d)/g, '$1 ') + const formatExp = (v: string) => { + const d = v.replace(/\D/g, '').slice(0, 4) + return d.length >= 3 ? `${d.slice(0, 2)}/${d.slice(2)}` : d + } + + // Step 1 — qualify + capture contact (saved immediately, active). + const handleSurvey = async (e: React.FormEvent) => { + e.preventDefault() + if (!income || !couple) { toast.error('Please answer both questions'); return } + if (!EMAIL_RE.test(email)) { toast.error('Please enter a valid email'); return } + const qualified = income === 'yes' && couple === 'yes' + setIsLoading(true) + try { + await fetch('/api/track/lead', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email, phone, + source_lp: qualified ? sourceLp : `${sourceLp}-unqualified`, + referral_code: tracking.ref, + utm_source: tracking.utm_source, + utm_medium: tracking.utm_medium, + utm_campaign: tracking.utm_campaign, + }), + }) + } catch { /* never block on a capture hiccup */ } + setIsLoading(false) + setStep(qualified ? 'checkout' : 'declined') + } + + // Step 2 — pay (email/phone already known). + const handlePayment = async (e: React.FormEvent) => { + e.preventDefault() + if (!cardNumber || !cardExp || !cardCvv || !zip) { + toast.error('Please enter your card details and ZIP') + return + } + setIsLoading(true) + setPaymentError(null) + try { + const signupRes = await fetch('/api/signup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email, phone, + amount: paymentType === 'monthly' ? PAYMENT_CONFIG.totalPrice : PAYMENT_CONFIG.oneTimePrice, + monthly_payment: PAYMENT_CONFIG.monthlyPrice, + payment_plan_months: paymentType === 'monthly' ? PAYMENT_CONFIG.totalMonths : 1, + source_lp: tracking.source_lp, + referral_code: tracking.ref, + utm_source: tracking.utm_source, + utm_medium: tracking.utm_medium, + utm_campaign: tracking.utm_campaign, + }), + }) + const signupData = await signupRes.json() + if (!signupRes.ok) throw new Error(signupData.error || 'Failed to create signup') + + const paymentRes = await fetch('/api/payment/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email, phone, zip, + cardNumber: cardNumber.replace(/\s/g, ''), + cardExp: cardExp.replace('/', ''), + cardCvv, + paymentType, + signupId: signupData.id, + }), + }) + const paymentData = await paymentRes.json() + if (!paymentRes.ok) { + setPaymentError(paymentData.error || 'Payment declined. Please check your card details and try again.') + return + } + setIsSuccess(true) + window.location.href = '/dashboard/login' + } catch (error) { + setPaymentError(error instanceof Error ? error.message : 'Something went wrong. Please try again.') + } finally { + setIsLoading(false) + } + } + + const ynToggle = (val: YN, set: (v: YN) => void) => ( +
+ {(['yes', 'no'] as const).map(opt => ( + + ))} +
+ ) + + if (isSuccess) { + return ( +
+ +

Welcome to Paradise!

+

Check your email for your vacation certificate. You can book your travel dates immediately!

+
+ ) + } + + return ( +
+
+

+ {step === 'checkout' ? 'Get your vacation certificate' : step === 'declined' ? "Thanks for checking!" : 'Do you qualify? (10 seconds)'} +

+

5 days / 4 nights all-inclusive · kids free

+
+ +
+ {step === 'survey' && ( +
+
+

1. Is your household income $70k+?

+ {ynToggle(income, setIncome)} +
+
+

2. Are you married or living with a partner?

+ {ynToggle(couple, setCouple)} +
+
+ setEmail(e.target.value)} + className="h-11 bg-white text-gray-900 border-gray-300 placeholder:text-gray-400" required /> + setPhone(e.target.value)} + className="h-11 bg-white text-gray-900 border-gray-300 placeholder:text-gray-400" /> +
+ +

No spam. No charge yet — this just checks your fit.

+
+ )} + + {step === 'declined' && ( +
+

+ This all-inclusive deal is reserved for households earning $70k+ traveling as a + couple, that's a requirement of the resort partners, not us. +

+

+ We've saved your info and will email you other Mexico deals that fit. 🌴 +

+ + Questions? Call (888) 602-2424 + +
+ )} + + {step === 'checkout' && ( +
+
✓ You qualify — lock in your deal below
+ + {/* Pricing toggle */} +
+ + +
+ +
+ Booking as {email} + +
+ +
+

Card details

+
+ setCardNumber(formatCardNumber(e.target.value))} + className="h-11 font-mono text-sm bg-white text-gray-900 border-gray-300 placeholder:text-gray-400" maxLength={19} required /> +
+ setCardExp(formatExp(e.target.value))} + className="h-11 font-mono text-sm bg-white text-gray-900 border-gray-300 placeholder:text-gray-400" maxLength={5} required /> + setCardCvv(e.target.value.replace(/\D/g, '').slice(0, 4))} + className="h-11 font-mono text-sm bg-white text-gray-900 border-gray-300 placeholder:text-gray-400" maxLength={4} required /> + setZip(e.target.value.replace(/\D/g, '').slice(0, 5))} + className="h-11 font-mono text-sm bg-white text-gray-900 border-gray-300 placeholder:text-gray-400" maxLength={5} required /> +
+
+
+ +
+
+ Today's charge: + ${amount} +
+ {paymentType === 'monthly' &&

Then ${PAYMENT_CONFIG.monthlyPrice}/mo × {PAYMENT_CONFIG.totalMonths - 1} more. Cancel anytime.

} +
100% refund within 30 days
+
+ + {paymentError && ( +
+ +

Payment failed

{paymentError}

+
+ )} + + + +
+ SSL secure + 30-day guarantee + 4.8★ +
+ +

+ {paymentType === 'monthly' + ? `By clicking Pay, you authorize $${PAYMENT_CONFIG.monthlyPrice} today and ${PAYMENT_CONFIG.totalMonths - 1} monthly charges of $${PAYMENT_CONFIG.monthlyPrice}. Cancel anytime. 100% refund within 30 days.` + : `By clicking Pay, you authorize $${PAYMENT_CONFIG.oneTimePrice}. 100% refund within 30 days.`} +

+
+ )} +
+
+ ) +} diff --git a/src/components/lp/shared/EbookCaptureForm.tsx b/src/components/lp/shared/EbookCaptureForm.tsx new file mode 100644 index 0000000..2e9ef64 --- /dev/null +++ b/src/components/lp/shared/EbookCaptureForm.tsx @@ -0,0 +1,481 @@ +'use client' + +import { useState, useEffect, useRef } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Loader2, Download, BookOpen, Lock, Shield, CreditCard, CheckCircle, + ShieldCheck, Star, Undo2, Plane, Sparkles, ArrowRight, X, Check, +} from 'lucide-react' +import { toast } from 'sonner' +import { PAYMENT_CONFIG } from '@/app/lp/_config/types' +import { useTrackingParams } from '@/hooks/useTrackingParams' +import { useEarlyLead } from '@/hooks/useEarlyLead' + +interface EbookCaptureFormProps { + variant?: 'inline' | 'card' | 'minimal' + accentColor?: string + buttonText?: string + sourceLp: string + className?: string + showIcon?: boolean +} + +export default function EbookCaptureForm({ + variant = 'inline', + accentColor = '#FF9800', + buttonText = 'Get My Free Guide', + sourceLp, + className = '', + showIcon = true, +}: EbookCaptureFormProps) { + const tracking = useTrackingParams(sourceLp) + const [email, setEmail] = useState('') + const [name, setName] = useState('') + const [phone, setPhone] = useState('') + const [firstName, setFirstName] = useState('') + const [lastName, setLastName] = useState('') + const [cardNumber, setCardNumber] = useState('') + const [cardExp, setCardExp] = useState('') + const [cardCvv, setCardCvv] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [isPaymentLoading, setIsPaymentLoading] = useState(false) + const [ebookSent, setEbookSent] = useState(false) + const [showPaymentModal, setShowPaymentModal] = useState(false) + const [paymentSuccess, setPaymentSuccess] = useState(false) + const [paymentError, setPaymentError] = useState(null) + const [paymentType, setPaymentType] = useState<'monthly' | 'one-time'>('one-time') + + const amount = paymentType === 'monthly' ? PAYMENT_CONFIG.monthlyPrice : PAYMENT_CONFIG.oneTimePrice + + const { captured: earlyCaptured, capture: captureNow } = useEarlyLead({ + email, phone, name: name || `${firstName} ${lastName}`.trim() || undefined, + source_lp: sourceLp, + referral_code: tracking.ref, + utm_source: tracking.utm_source, + utm_medium: tracking.utm_medium, + utm_campaign: tracking.utm_campaign, + }) + + // Lock body scroll when modal open + useEffect(() => { + if (showPaymentModal) { + document.body.style.overflow = 'hidden' + } else { + document.body.style.overflow = '' + } + return () => { document.body.style.overflow = '' } + }, [showPaymentModal]) + + const formatCardNumber = (value: string) => { + const digits = value.replace(/\D/g, '').slice(0, 16) + return digits.replace(/(\d{4})(?=\d)/g, '$1 ') + } + + const formatExp = (value: string) => { + const digits = value.replace(/\D/g, '').slice(0, 4) + if (digits.length >= 3) return `${digits.slice(0, 2)}/${digits.slice(2)}` + return digits + } + + const handleEbookSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!email) { + toast.error('Please enter your email') + return + } + + setIsLoading(true) + try { + const res = await fetch('/api/claim', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email, name, phone, + source_lp: sourceLp, + referral_code: tracking.ref, + utm_source: tracking.utm_source, + utm_medium: tracking.utm_medium, + utm_campaign: tracking.utm_campaign, + }), + }) + + const data = await res.json() + if (data.success) { + // Double opt-in: no direct download. The PDF link is emailed; the user + // must click it to confirm (verify) and unlock the guide. + setEbookSent(true) + toast.success('Check your email to confirm and get your guide!') + } else { + throw new Error(data.error || 'Failed') + } + } catch (error) { + console.error('Ebook error:', error) + toast.error('Something went wrong. Please try again.') + } finally { + setIsLoading(false) + } + } + + const handlePayment = async (e: React.FormEvent) => { + e.preventDefault() + if (!firstName || !lastName || !cardNumber || !cardExp || !cardCvv) { + toast.error('Please fill in all fields') + return + } + + setIsPaymentLoading(true) + setPaymentError(null) + try { + const signupRes = await fetch('/api/signup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email, + full_name: `${firstName} ${lastName}`, + phone, + amount: paymentType === 'monthly' ? PAYMENT_CONFIG.totalPrice : PAYMENT_CONFIG.oneTimePrice, + monthly_payment: PAYMENT_CONFIG.monthlyPrice, + payment_plan_months: paymentType === 'monthly' ? PAYMENT_CONFIG.totalMonths : 1, + source_lp: tracking.source_lp, referral_code: tracking.ref, + utm_source: tracking.utm_source, utm_medium: tracking.utm_medium, utm_campaign: tracking.utm_campaign, + }), + }) + const signupData = await signupRes.json() + if (!signupRes.ok) throw new Error(signupData.error || 'Signup failed') + + const paymentRes = await fetch('/api/payment/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + firstName, lastName, email, phone, + cardNumber: cardNumber.replace(/\s/g, ''), + cardExp: cardExp.replace('/', ''), + cardCvv, + paymentType, + signupId: signupData.id, + }), + }) + const paymentData = await paymentRes.json() + if (!paymentRes.ok) { + setPaymentError(paymentData.error || 'Payment declined. Please check your card details and try again.') + return + } + + setPaymentSuccess(true) + setShowPaymentModal(false) + // Redirect to payment success page + window.location.href = '/dashboard/login' + } catch (error) { + setPaymentError(error instanceof Error ? error.message : 'Something went wrong. Please try again.') + } finally { + setIsPaymentLoading(false) + } + } + + // After payment success + if (paymentSuccess) { + return ( +
+ +

Welcome to Paradise!

+

+ Your guide was downloaded and your vacation certificate is confirmed! + {paymentType === 'monthly' && ` Next payment of $${PAYMENT_CONFIG.monthlyPrice} in 30 days.`} +

+
+ ) + } + + // After lead captured — double opt-in: tell them to check their email + if (ebookSent) { + return ( +
+ +

Check your email!

+

+ We sent a confirmation link to {email}. Click it to confirm + your address and unlock your free guide. +

+
+ ) + } + + // ── EBOOK CAPTURE FORM (card variant) ── + const formInputs = ( +
+ {variant === 'card' && ( + setName(e.target.value)} + className="h-11 bg-white text-gray-900 border-gray-300 placeholder:text-gray-400" + /> + )} +
+ setEmail(e.target.value)} + onBlur={captureNow} + className={`${variant === 'inline' ? 'h-12 flex-1 text-base' : 'h-11'} bg-white text-gray-900 border-gray-300 placeholder:text-gray-400 ${earlyCaptured ? 'pr-9' : ''}`} + required + /> + {earlyCaptured && ( + + )} +
+ + {variant === 'card' &&

No spam. Unsubscribe anytime.

} +
+ ) + + if (variant === 'card') { + return ( + <> +
+ {showIcon && } +

Free E-book: Budget Luxury Travel

+

5 secrets to luxury Mexico vacations on a budget

+ {formInputs} +
+ {renderPaymentModal()} + + ) + } + + // Inline variant + return ( + <> +
+
+ setEmail(e.target.value)} + onBlur={captureNow} + className={`h-12 text-base bg-white text-gray-900 border-gray-300 placeholder:text-gray-400 ${earlyCaptured ? 'pr-9' : ''}`} + required + /> + {earlyCaptured && ( + + )} +
+ +
+ {renderPaymentModal()} + + ) + + // ── PAYMENT MODAL (same design as PayNowForm) ── + function renderPaymentModal() { + if (!showPaymentModal) return null + + return ( +
+
setShowPaymentModal(false)} /> + +
+ {/* Close */} + + + {/* Hero */} +
+
+ SPECIAL OFFER — JUST FOR YOU +
+

+ Wait! Your Guide is Downloading... +

+

+ Ready to actually go? Here's how the vacation works: +

+
+ + {/* 100% Money Back */} +
+
+ +
+

100% MONEY-BACK GUARANTEE

+

30 days. No questions. Full refund.

+
+
+
+ +
+ {/* Pricing */} +
+ + +
+ + {/* Book immediately */} +
+ +

Book your vacation RIGHT AFTER first payment!

+
+ + {/* Includes */} +
+ {['5 Days / 4 Nights', 'All-Inclusive', 'Unlimited Food', '4 Destinations', 'Flexible Dates', 'Book Immediately'].map(item => ( + + {item} + + ))} +
+ + {/* Form */} +
+
+ Booking: {email} +
+ +
+
+ + setFirstName(e.target.value)} + className="h-10 text-sm bg-white text-gray-900 border-gray-300" required /> +
+
+ + setLastName(e.target.value)} + className="h-10 text-sm bg-white text-gray-900 border-gray-300" required /> +
+
+ +
+ + setPhone(e.target.value)} + className="h-10 text-sm bg-white text-gray-900 border-gray-300" /> +
+ +
+

+ Card Details +

+
+ setCardNumber(formatCardNumber(e.target.value))} + className="h-10 text-sm font-mono bg-white text-gray-900 border-gray-300 placeholder:text-gray-400" + maxLength={19} required /> +
+ setCardExp(formatExp(e.target.value))} + className="h-10 text-sm font-mono bg-white text-gray-900 border-gray-300 placeholder:text-gray-400" + maxLength={5} required /> + setCardCvv(e.target.value.replace(/\D/g, '').slice(0, 4))} + className="h-10 text-sm font-mono bg-white text-gray-900 border-gray-300 placeholder:text-gray-400" + maxLength={4} required /> +
+
+
+ + {/* Charge summary */} +
+
+ Today's charge: +
+ {paymentType === 'monthly' ? '$59' : '$599'} + ${amount} +
+
+ {paymentType === 'monthly' && ( +

Then ${PAYMENT_CONFIG.monthlyPrice}/mo × {PAYMENT_CONFIG.totalMonths - 1} more. Cancel anytime.

+ )} +
+ 100% refund within 30 days +
+
+ + {/* Error message */} + {paymentError && ( +
+ +
+

Payment Failed

+

{paymentError}

+

Please check your card details and try again.

+
+
+ )} + + + +
+ SSL + 30-Day Guarantee + 4.8★ +
+ +

+ {paymentType === 'monthly' + ? `By clicking Pay, you authorize $${PAYMENT_CONFIG.monthlyPrice} today and ${PAYMENT_CONFIG.totalMonths - 1} monthly charges of $${PAYMENT_CONFIG.monthlyPrice}. Cancel anytime. 100% refund within 30 days.` + : `By clicking Pay, you authorize $${PAYMENT_CONFIG.oneTimePrice}. 100% refund within 30 days.`} +

+ + +
+
+
+
+ ) + } +} diff --git a/src/components/lp/shared/FAQAccordion.tsx b/src/components/lp/shared/FAQAccordion.tsx new file mode 100644 index 0000000..e92fe26 --- /dev/null +++ b/src/components/lp/shared/FAQAccordion.tsx @@ -0,0 +1,38 @@ +'use client' + +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion' +import { FAQ_ITEMS } from '@/app/lp/_config/types' + +interface FAQAccordionProps { + items?: { question: string; answer: string }[] + className?: string + variant?: 'default' | 'dark' +} + +export default function FAQAccordion({ + items = FAQ_ITEMS, + className = '', + variant = 'default', +}: FAQAccordionProps) { + const isDark = variant === 'dark' + + return ( + + {items.map((item, i) => ( + + + {item.question} + + + {item.answer} + + + ))} + + ) +} diff --git a/src/components/lp/shared/FloatingPhoneButton.tsx b/src/components/lp/shared/FloatingPhoneButton.tsx new file mode 100644 index 0000000..59c3d6a --- /dev/null +++ b/src/components/lp/shared/FloatingPhoneButton.tsx @@ -0,0 +1,19 @@ +'use client' + +import { Phone } from 'lucide-react' + +const PHONE_NUMBER = '+18886022424' +const PHONE_DISPLAY = '(888) 602-2424' + +export default function FloatingPhoneButton() { + return ( + + + {PHONE_DISPLAY} + + ) +} diff --git a/src/components/lp/shared/InfluencerBuzz.tsx b/src/components/lp/shared/InfluencerBuzz.tsx new file mode 100644 index 0000000..6080fcb --- /dev/null +++ b/src/components/lp/shared/InfluencerBuzz.tsx @@ -0,0 +1,168 @@ +'use client' + +import { Heart, MessageCircle, Share2, Play, Eye } from 'lucide-react' + +const INFLUENCERS = [ + { + handle: '@travel.to.mexico8', + followers: '45.2K', + avatar: '/images/cdn/photo-1494790108377-be9c29b29330.jpg', + caption: 'OMG this Mexico deal is INSANE 🤯 $29/mo for all-inclusive?! I booked immediately...', + likes: '12.8K', + comments: '1.4K', + views: '340K', + image: '/images/cdn/photo-1507525428034-b723cf961d3e.jpg', + }, + { + handle: '@chris724santos', + followers: '89.1K', + avatar: '/images/cdn/photo-1500648767791-00dcc994a43e.jpg', + caption: 'We booked the $29/mo Mexico vacation everyone on TikTok is talking about. NO REGRETS 🌴', + likes: '24.3K', + comments: '3.1K', + views: '892K', + image: '/images/cdn/photo-1552074284-5e88ef1aef18.jpg', + }, + { + handle: '@vacay.deals', + followers: '1.2M', + avatar: '/images/cdn/photo-1438761681033-6461ffad8d80.jpg', + caption: 'STOP SCROLLING. All-inclusive Mexico for $29/mo. This is NOT a drill 🚨🏖️', + likes: '67.5K', + comments: '5.8K', + views: '2.4M', + image: '/images/cdn/photo-1580846629083-02669741360a.jpg', + }, + { + handle: '@budget.luxe', + followers: '312K', + avatar: '/images/cdn/photo-1544005313-94ddf0286df2.jpg', + caption: 'Just got back from Cancun. $29/mo for THIS?? The math is mathing 💅✨', + likes: '31.2K', + comments: '2.7K', + views: '1.1M', + image: '/images/cdn/photo-1519046904884-53103b34b206.jpg', + }, + { + handle: '@couples.getaway', + followers: '567K', + avatar: '/images/cdn/photo-1506794778202-cad84cf45f1d.jpg', + caption: 'My wife found this deal and I did NOT believe her. Cabo for $29/mo?! It was REAL 🤩', + likes: '45.9K', + comments: '4.2K', + views: '1.8M', + image: '/images/cdn/photo-1571896349842-33c89424de2d.jpg', + }, + { + handle: '@travelwithsam', + followers: '203K', + avatar: '/images/cdn/photo-1522529599102-193c0d76b5b6.jpg', + caption: 'Puerto Vallarta sunset from our all-inclusive resort. $29/mo. I still can\'t believe it 🌅', + likes: '18.7K', + comments: '1.9K', + views: '678K', + image: '/images/cdn/photo-1585793753011-397e6e4668d6.jpg', + }, +] + +interface InfluencerBuzzProps { + className?: string + variant?: 'light' | 'dark' + count?: number + title?: string +} + +export default function InfluencerBuzz({ + className = '', + variant = 'light', + count = 3, + title = 'Everyone\'s Talking About Us', +}: InfluencerBuzzProps) { + const isDark = variant === 'dark' + // Pick influencers — rotate based on component render + const displayed = INFLUENCERS.slice(0, count) + + return ( +
+
+ {/* Header */} +
+
+ + GOING VIRAL +
+

+ {title} +

+

+ Real people. Real vacations. Real reactions. +

+
+ + {/* Stats bar */} +
+
+

7.3M+

+

Views

+
+
+
+

242

+

Videos

+
+
+
+

2,847+

+

Certificates Claimed

+
+
+ + {/* Influencer cards */} +
= 3 ? 'sm:grid-cols-3' : count === 2 ? 'sm:grid-cols-2' : ''}`}> + {displayed.map((inf, i) => ( +
+ {/* Creator header */} +
+ {inf.handle} +
+

{inf.handle}

+

{inf.followers} followers

+
+ +
+ + {/* Video thumbnail */} +
+ Vacation +
+
+ +
+
+ {/* View count */} +
+ + {inf.views} +
+ {/* Caption overlay */} +
+

{inf.caption}

+
+
+ + {/* Engagement */} +
+ {inf.likes} + {inf.comments} + Share +
+
+ ))} +
+
+
+ ) +} diff --git a/src/components/lp/shared/PayNowForm.tsx b/src/components/lp/shared/PayNowForm.tsx new file mode 100644 index 0000000..31e13a5 --- /dev/null +++ b/src/components/lp/shared/PayNowForm.tsx @@ -0,0 +1,471 @@ +'use client' + +import { useState, useEffect, useRef } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Loader2, Lock, Shield, CreditCard, CheckCircle, ShieldCheck, + Star, Clock, Undo2, Plane, Sparkles, ArrowRight, X, Check, +} from 'lucide-react' +import { toast } from 'sonner' +import { PAYMENT_CONFIG } from '@/app/lp/_config/types' +import CountdownTimer from './CountdownTimer' +import { useTrackingParams } from '@/hooks/useTrackingParams' +import { useEarlyLead } from '@/hooks/useEarlyLead' + +interface PayNowFormProps { + variant?: 'default' | 'minimal' | 'dark' | 'inline' + accentColor?: string + buttonText?: string + showToggle?: boolean + className?: string + sourceLp?: string + onSuccess?: () => void +} + +export default function PayNowForm({ + variant = 'default', + accentColor = '#E8651A', + buttonText = 'Get My Vacation Certificate!', + showToggle = true, + className = '', + sourceLp, + onSuccess, +}: PayNowFormProps) { + const tracking = useTrackingParams(sourceLp) + const [email, setEmail] = useState('') + const [phone, setPhone] = useState('') + const [firstName, setFirstName] = useState('') + const [lastName, setLastName] = useState('') + const [cardNumber, setCardNumber] = useState('') + const [cardExp, setCardExp] = useState('') + const [cardCvv, setCardCvv] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [isSuccess, setIsSuccess] = useState(false) + const [paymentType, setPaymentType] = useState<'monthly' | 'one-time'>('monthly') + const [showPaymentModal, setShowPaymentModal] = useState(false) + const [paymentError, setPaymentError] = useState(null) + const modalRef = useRef(null) + + const amount = paymentType === 'monthly' ? PAYMENT_CONFIG.monthlyPrice : PAYMENT_CONFIG.oneTimePrice + + const { captured: earlyCaptured, capture: captureNow } = useEarlyLead({ + email, phone, name: `${firstName} ${lastName}`.trim() || undefined, + source_lp: sourceLp, + referral_code: tracking.ref, + utm_source: tracking.utm_source, + utm_medium: tracking.utm_medium, + utm_campaign: tracking.utm_campaign, + }) + + // Lock body scroll when modal open + useEffect(() => { + if (showPaymentModal) { + document.body.style.overflow = 'hidden' + } else { + document.body.style.overflow = '' + } + return () => { document.body.style.overflow = '' } + }, [showPaymentModal]) + + const formatCardNumber = (value: string) => { + const digits = value.replace(/\D/g, '').slice(0, 16) + return digits.replace(/(\d{4})(?=\d)/g, '$1 ') + } + + const formatExp = (value: string) => { + const digits = value.replace(/\D/g, '').slice(0, 4) + if (digits.length >= 3) return `${digits.slice(0, 2)}/${digits.slice(2)}` + return digits + } + + const handleLeadCapture = async (e: React.FormEvent) => { + e.preventDefault() + if (!email || !phone) { + toast.error('Please enter your email and phone number') + return + } + setIsLoading(true) + try { + // Fire claim — saves to DB, sends PDF email. Don't block payment modal + // on email send failures (rare). + const res = await fetch('/api/claim', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email, phone, + source_lp: sourceLp, + referral_code: tracking.ref, + utm_source: tracking.utm_source, + utm_medium: tracking.utm_medium, + utm_campaign: tracking.utm_campaign, + }), + }) + const data = await res.json().catch(() => ({})) + if (data?.emailSent) { + toast.success('Free guide is on its way to your inbox!') + } + } catch (err) { + console.error('claim error:', err) + } finally { + setIsLoading(false) + setShowPaymentModal(true) + } + } + + const handlePayment = async (e: React.FormEvent) => { + e.preventDefault() + if (!firstName || !lastName || !cardNumber || !cardExp || !cardCvv) { + toast.error('Please fill in all fields') + return + } + + setIsLoading(true) + setPaymentError(null) + try { + const signupRes = await fetch('/api/signup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email, + full_name: `${firstName} ${lastName}`, + phone, + amount: paymentType === 'monthly' ? PAYMENT_CONFIG.totalPrice : PAYMENT_CONFIG.oneTimePrice, + monthly_payment: PAYMENT_CONFIG.monthlyPrice, + payment_plan_months: paymentType === 'monthly' ? PAYMENT_CONFIG.totalMonths : 1, + source_lp: tracking.source_lp, + referral_code: tracking.ref, + utm_source: tracking.utm_source, + utm_medium: tracking.utm_medium, + utm_campaign: tracking.utm_campaign, + }), + }) + const signupData = await signupRes.json() + if (!signupRes.ok) throw new Error(signupData.error || 'Failed to create signup') + + const paymentRes = await fetch('/api/payment/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + firstName, lastName, email, phone, + cardNumber: cardNumber.replace(/\s/g, ''), + cardExp: cardExp.replace('/', ''), + cardCvv, + paymentType, + signupId: signupData.id, + }), + }) + const paymentData = await paymentRes.json() + if (!paymentRes.ok) { + const errMsg = paymentData.error || 'Payment declined. Please check your card details and try again.' + setPaymentError(errMsg) + return + } + + setIsSuccess(true) + setShowPaymentModal(false) + onSuccess?.() + // Redirect to payment success page + window.location.href = '/dashboard/login' + } catch (error) { + const errMsg = error instanceof Error ? error.message : 'Something went wrong. Please try again.' + setPaymentError(errMsg) + } finally { + setIsLoading(false) + } + } + + if (isSuccess) { + return ( +
+ +

Welcome to Paradise!

+

+ Check your email for your vacation certificate. You can book your travel dates immediately! + {paymentType === 'monthly' && ` Next payment of $${PAYMENT_CONFIG.monthlyPrice} in 30 days.`} +

+
+ ) + } + + return ( + <> + {/* Step 1: Email + Phone inline capture — always light bg inputs */} +
+
+
+ +
+ setEmail(e.target.value)} + onBlur={captureNow} + className={`h-12 text-base bg-white text-gray-900 border-gray-300 placeholder:text-gray-400 ${earlyCaptured ? 'pr-9' : ''}`} + required + /> + {earlyCaptured && ( + + )} +
+
+
+ + setPhone(e.target.value)} + onBlur={captureNow} + className="h-12 text-base bg-white text-gray-900 border-gray-300 placeholder:text-gray-400" + required + /> +
+ +
+ Secure + 100% Money-Back +
+
+
+ + {/* Step 2: Full-screen payment modal — custom implementation for mobile */} + {showPaymentModal && ( +
+ {/* Backdrop */} +
setShowPaymentModal(false)} /> + + {/* Modal — full screen on mobile, centered card on desktop */} +
+ {/* Close button */} + + + {/* Hero header — compact on mobile */} +
+
+ EXCLUSIVE OFFER +
+

+ All-Inclusive Mexico Vacation +

+

5 Days • 4 Nights • All Meals & Drinks

+
+ + Expires in + +
+
+ + {/* 100% Money Back — compact */} +
+
+ +
+

100% MONEY-BACK GUARANTEE

+

30 days. No questions. Full refund.

+
+
+
+ +
+ + {/* Pricing toggle — compact */} + {showToggle && ( +
+ + +
+ )} + + {/* Book immediately callout */} +
+ +

Book your vacation RIGHT AFTER first payment!

+
+ + {/* What's included — compact horizontal */} +
+ {['5 Days / 4 Nights', 'All-Inclusive', 'Unlimited Food', '4 Destinations', 'Flexible Dates', 'Book Immediately'].map(item => ( + + {item} + + ))} +
+ + {/* Payment form */} +
+ {/* Booking info */} +
+ Booking: {email} +
+ + {/* Name fields */} +
+
+ + setFirstName(e.target.value)} + className="h-10 text-sm bg-white text-gray-900 border-gray-300" required /> +
+
+ + setLastName(e.target.value)} + className="h-10 text-sm bg-white text-gray-900 border-gray-300" required /> +
+
+ + {/* Card fields — standard HTML inputs, no Collect.js iframe issues */} +
+

+ Card Details +

+
+ setCardNumber(formatCardNumber(e.target.value))} + className="h-10 text-sm font-mono bg-white text-gray-900 border-gray-300 placeholder:text-gray-400" + maxLength={19} + required + /> +
+ setCardExp(formatExp(e.target.value))} + className="h-10 text-sm font-mono bg-white text-gray-900 border-gray-300 placeholder:text-gray-400" + maxLength={5} + required + /> + setCardCvv(e.target.value.replace(/\D/g, '').slice(0, 4))} + className="h-10 text-sm font-mono bg-white text-gray-900 border-gray-300 placeholder:text-gray-400" + maxLength={4} + required + /> +
+
+
+ + {/* Charge summary — compact */} +
+
+ Today's charge: +
+ {paymentType === 'monthly' ? '$59' : '$599'} + ${amount} +
+
+ {paymentType === 'monthly' && ( +

Then ${PAYMENT_CONFIG.monthlyPrice}/mo × {PAYMENT_CONFIG.totalMonths - 1} more. Cancel anytime.

+ )} +
+ 100% refund within 30 days +
+
+ + {/* Error message */} + {paymentError && ( +
+ +
+

Payment Failed

+

{paymentError}

+

Please check your card details and try again.

+
+
+ )} + + {/* Pay button */} + + + {/* Trust strip */} +
+ SSL + 30-Day Guarantee + 4.8★ +
+ +

+ {paymentType === 'monthly' + ? `By clicking Pay, you authorize $${PAYMENT_CONFIG.monthlyPrice} today and ${PAYMENT_CONFIG.totalMonths - 1} monthly charges of $${PAYMENT_CONFIG.monthlyPrice}. Cancel anytime. 100% refund within 30 days.` + : `By clicking Pay, you authorize $${PAYMENT_CONFIG.oneTimePrice}. 100% refund within 30 days.`} +

+
+
+
+
+ )} + + ) +} diff --git a/src/components/lp/shared/PricingDisplay.tsx b/src/components/lp/shared/PricingDisplay.tsx new file mode 100644 index 0000000..80bce77 --- /dev/null +++ b/src/components/lp/shared/PricingDisplay.tsx @@ -0,0 +1,79 @@ +'use client' + +import { useState } from 'react' +import { PAYMENT_CONFIG } from '@/app/lp/_config/types' + +interface PricingDisplayProps { + variant?: 'default' | 'large' | 'compact' | 'dark' + accentColor?: string + className?: string + showComparison?: boolean +} + +export default function PricingDisplay({ + variant = 'default', + accentColor = '#E8651A', + className = '', + showComparison = true, +}: PricingDisplayProps) { + const [selected, setSelected] = useState<'monthly' | 'one-time'>('monthly') + const isDark = variant === 'dark' + + return ( +
+
+ + +
+ +
+ {selected === 'monthly' ? ( + <> +
+ ${PAYMENT_CONFIG.monthlyPrice}/mo +
+

+ for {PAYMENT_CONFIG.totalMonths} months (${PAYMENT_CONFIG.totalPrice} total) +

+ + ) : ( + <> +
+ ${PAYMENT_CONFIG.oneTimePrice} +
+

+ one-time payment — save ${PAYMENT_CONFIG.totalPrice - PAYMENT_CONFIG.oneTimePrice} vs. monthly! +

+ + )} +
+ + {showComparison && ( +
+ Regular price: $1,500+ + + Save over $1,100! + +
+ )} +
+ ) +} diff --git a/src/components/lp/shared/SocialProofTicker.tsx b/src/components/lp/shared/SocialProofTicker.tsx new file mode 100644 index 0000000..163c08f --- /dev/null +++ b/src/components/lp/shared/SocialProofTicker.tsx @@ -0,0 +1,68 @@ +'use client' + +import { useState, useEffect } from 'react' +import { motion, AnimatePresence } from 'framer-motion' + +const NAMES = [ + 'Maria from TX', 'James from FL', 'Sarah from CA', 'David from NY', + 'Jennifer from IL', 'Robert from AZ', 'Lisa from CO', 'Michael from GA', + 'Rachel from PA', 'Chris from WA', 'Amanda from NC', 'Daniel from OH', + 'Emily from VA', 'Brian from NJ', 'Nicole from TN', 'Kevin from MN', +] + +const ACTIONS = [ + 'just claimed their certificate', + 'just signed up', + 'booked Cancun', + 'booked Cabo', + 'chose Riviera Maya', + 'is heading to Puerto Vallarta', +] + +interface SocialProofTickerProps { + className?: string + bgColor?: string + textColor?: string + interval?: number +} + +export default function SocialProofTicker({ + className = '', + bgColor = '#000000', + textColor = '#FFFFFF', + interval = 5000, +}: SocialProofTickerProps) { + const [current, setCurrent] = useState(0) + + useEffect(() => { + const timer = setInterval(() => { + setCurrent(prev => (prev + 1) % NAMES.length) + }, interval) + return () => clearInterval(timer) + }, [interval]) + + const name = NAMES[current % NAMES.length] + const action = ACTIONS[current % ACTIONS.length] + + return ( + + ) +} diff --git a/src/components/lp/shared/StickyMobileCTA.tsx b/src/components/lp/shared/StickyMobileCTA.tsx new file mode 100644 index 0000000..49cfbbe --- /dev/null +++ b/src/components/lp/shared/StickyMobileCTA.tsx @@ -0,0 +1,74 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Phone } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { PAYMENT_CONFIG } from '@/app/lp/_config/types' +import CountdownTimer from './CountdownTimer' + +interface StickyMobileCTAProps { + accentColor?: string + buttonText?: string + targetId?: string + className?: string +} + +export default function StickyMobileCTA({ + accentColor = '#E8651A', + buttonText = 'Lock In This Price', + targetId = 'signup-form', + className = '', +}: StickyMobileCTAProps) { + const [visible, setVisible] = useState(false) + + useEffect(() => { + const handleScroll = () => { + setVisible(window.scrollY > 400) + } + window.addEventListener('scroll', handleScroll, { passive: true }) + return () => window.removeEventListener('scroll', handleScroll) + }, []) + + const scrollToForm = () => { + document.getElementById(targetId)?.scrollIntoView({ behavior: 'smooth' }) + } + + return ( +
+
+ {/* Urgency micro-bar */} +
+

+ + OFFER EXPIRES IN + +

+
+
+ + + +
+

$59/mo regular

+

${PAYMENT_CONFIG.monthlyPrice}/mo

+
+ +
+
+
+ ) +} diff --git a/src/components/lp/shared/TestimonialCard.tsx b/src/components/lp/shared/TestimonialCard.tsx new file mode 100644 index 0000000..8e7398e --- /dev/null +++ b/src/components/lp/shared/TestimonialCard.tsx @@ -0,0 +1,53 @@ +'use client' + +import { Star } from 'lucide-react' + +interface TestimonialCardProps { + quote: string + name: string + location: string + photo?: string + variant?: 'default' | 'dark' | 'minimal' + className?: string +} + +export default function TestimonialCard({ + quote, + name, + location, + photo, + variant = 'default', + className = '', +}: TestimonialCardProps) { + const isDark = variant === 'dark' + + return ( +
+
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+

+ “{quote}” +

+
+ {photo && ( + {name} + )} +
+

{name}

+

{location}

+
+
+
+ ) +} diff --git a/src/components/lp/shared/TikTokCarousel.tsx b/src/components/lp/shared/TikTokCarousel.tsx new file mode 100644 index 0000000..63897b9 --- /dev/null +++ b/src/components/lp/shared/TikTokCarousel.tsx @@ -0,0 +1,171 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import useEmblaCarousel from 'embla-carousel-react' +import { ChevronLeft, ChevronRight, Play } from 'lucide-react' +import videosRaw from '@/data/tiktok-videos.json' + +interface VideoMeta { + id: string + username: string + title: string +} + +const ALL_VIDEOS: VideoMeta[] = videosRaw as VideoMeta[] +const DEFAULT_USERNAME = 'travel.to.mexico8' + +function shuffle(arr: T[]): T[] { + const a = [...arr] + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[a[i], a[j]] = [a[j], a[i]] + } + return a +} + +interface TikTokCarouselProps { + username?: string + className?: string + title?: string + subtitle?: string + count?: number +} + +function TikTokTile({ video }: { video: VideoMeta }) { + const href = `https://www.tiktok.com/@${video.username}/video/${video.id}` + return ( + + {video.title + {/* Dim overlay on hover */} +
+ + {/* TikTok corner badge */} +
+ + + + TikTok +
+ + {/* Center play button */} +
+
+ +
+
+ + {/* Bottom gradient + caption */} +
+

@{video.username}

+ {video.title && ( +

{video.title}

+ )} +
+
+ ) +} + +export default function TikTokCarousel({ + username = DEFAULT_USERNAME, + className = '', + title = 'See Real Vacationers in Paradise', + subtitle = 'Watch what our travelers are experiencing right now', + count = 10, +}: TikTokCarouselProps) { + const videos = useMemo(() => shuffle(ALL_VIDEOS).slice(0, count), [count]) + + const [emblaRef, emblaApi] = useEmblaCarousel({ + loop: false, + align: 'start', + slidesToScroll: 1, + containScroll: 'trimSnaps', + }) + const [canScrollPrev, setCanScrollPrev] = useState(false) + const [canScrollNext, setCanScrollNext] = useState(true) + + useEffect(() => { + if (!emblaApi) return + const onSelect = () => { + setCanScrollPrev(emblaApi.canScrollPrev()) + setCanScrollNext(emblaApi.canScrollNext()) + } + emblaApi.on('select', onSelect) + emblaApi.on('reInit', onSelect) + onSelect() + return () => { emblaApi.off('select', onSelect); emblaApi.off('reInit', onSelect) } + }, [emblaApi]) + + if (videos.length === 0) return null + + return ( +
+
+
+
+ + + + {ALL_VIDEOS.length} Real Videos +
+

{title}

+

{subtitle}

+
+ +
+
+
+ {videos.map((v) => ( +
+ +
+ ))} +
+
+ + {canScrollPrev && ( + + )} + {canScrollNext && ( + + )} +
+ + +
+
+ ) +} diff --git a/src/components/lp/shared/TopPhoneBar.tsx b/src/components/lp/shared/TopPhoneBar.tsx new file mode 100644 index 0000000..f3378f9 --- /dev/null +++ b/src/components/lp/shared/TopPhoneBar.tsx @@ -0,0 +1,20 @@ +'use client' + +import { Phone } from 'lucide-react' + +export default function TopPhoneBar() { + return ( + + ) +} diff --git a/src/components/lp/shared/TrustBadges.tsx b/src/components/lp/shared/TrustBadges.tsx new file mode 100644 index 0000000..40b7eef --- /dev/null +++ b/src/components/lp/shared/TrustBadges.tsx @@ -0,0 +1,60 @@ +'use client' + +import { Shield, Lock, Star, BadgeCheck, CreditCard } from 'lucide-react' + +interface TrustBadgesProps { + variant?: 'horizontal' | 'vertical' | 'compact' + className?: string + textColor?: string +} + +export default function TrustBadges({ + variant = 'horizontal', + className = '', + textColor = '#6B7280', +}: TrustBadgesProps) { + const badges = [ + { icon: Lock, label: '256-bit SSL' }, + { icon: Shield, label: '30-Day Guarantee' }, + { icon: Star, label: '4.8/5 Rating' }, + { icon: BadgeCheck, label: 'Verified Business' }, + { icon: CreditCard, label: 'Secure Payments' }, + ] + + if (variant === 'compact') { + return ( +
+ {badges.slice(0, 3).map((b) => ( + + + {b.label} + + ))} +
+ ) + } + + if (variant === 'vertical') { + return ( +
+ {badges.map((b) => ( +
+ + {b.label} +
+ ))} +
+ ) + } + + return ( +
+ {badges.map((b) => ( +
+ + {b.label} +
+ ))} +
+ ) +} diff --git a/src/components/lp/shared/UrgencyBanner.tsx b/src/components/lp/shared/UrgencyBanner.tsx new file mode 100644 index 0000000..6c3e63e --- /dev/null +++ b/src/components/lp/shared/UrgencyBanner.tsx @@ -0,0 +1,97 @@ +'use client' + +import { useState, useEffect } from 'react' +import { X, AlertTriangle, Clock, Users } from 'lucide-react' +import CountdownTimer from './CountdownTimer' + +interface UrgencyBannerProps { + variant?: 'countdown' | 'scarcity' | 'social' | 'combined' + className?: string + dismissible?: boolean +} + +// Psychological triggers: +// 1. Loss aversion — "Don't miss out" > "Get this deal" +// 2. Social proof — others are buying right now +// 3. Scarcity — limited quantity +// 4. Time pressure — countdown creates urgency +// 5. Anchoring — show the "real" price first + +const SCARCITY_MESSAGES = [ + '🔥 Only 7 certificates left at this price', + '⚡ 14 people are viewing this offer right now', + '🎯 23 certificates claimed in the last hour', + '⏰ Price increases to $59/mo after this promotion ends', +] + +export default function UrgencyBanner({ + variant = 'combined', + className = '', + dismissible = true, +}: UrgencyBannerProps) { + const [dismissed, setDismissed] = useState(false) + const [messageIdx, setMessageIdx] = useState(0) + const [viewerCount] = useState(() => Math.floor(Math.random() * 12) + 8) + + useEffect(() => { + if (variant === 'social' || variant === 'combined') { + const timer = setInterval(() => { + setMessageIdx(prev => (prev + 1) % SCARCITY_MESSAGES.length) + }, 4000) + return () => clearInterval(timer) + } + }, [variant]) + + if (dismissed) return null + + return ( +
+ {/* Main urgency bar */} +
+
+ {variant === 'countdown' || variant === 'combined' ? ( + <> + + ⚠️ PROMOTIONAL PRICING EXPIRES IN + EXPIRES IN + + — Lock in $29/mo before it's gone + + ) : variant === 'scarcity' ? ( + <> + + {SCARCITY_MESSAGES[messageIdx]} + + ) : ( + <> + + {viewerCount} people viewing + + + + left at this price + + )} +
+ + {dismissible && ( + + )} +
+ + {/* Secondary social proof ticker (combined only) */} + {variant === 'combined' && ( +
+

+ {SCARCITY_MESSAGES[messageIdx]} +

+
+ )} +
+ ) +} diff --git a/src/data/tiktok-videos.json b/src/data/tiktok-videos.json new file mode 100644 index 0000000..7603b0e --- /dev/null +++ b/src/data/tiktok-videos.json @@ -0,0 +1,1212 @@ +[ + { + "id": "7618023828597312788", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7618023673798151444", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7618023580994948372", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7618023404637130005", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7618023226857311508", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7616902964158008596", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7616902684678917397", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7616902503128501524", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7616902289504193812", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7616902084092480789", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7616536467678055701", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7616536245102972180", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7616536083496455445", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7616535801245093141", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7616535620449619220", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7616165126718115093", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7616164954739182869", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7616164791706717461", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7616164636920007957", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7616164443701103893", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7615793139785288980", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7615792939800808725", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7615792741502487828", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7615792557251071253", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7615792335791787285", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7615418268869741845", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7615418100892192020", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7615417898932243732", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7615417606446632213", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7615417396140068116", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7614307089007021332", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7614306978164264213", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7614306835511790868", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7614306744541383957", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7614306570301738260", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7613939774008593684", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7613939542713715989", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7613939329387220245", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7613939116627086613", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7613938879388798228", + "username": "travel.to.mexico8", + "title": "🌴 MEXICO FAMILY VACATION FOR ONLY $299! 🇲🇽 5 Days & 4 Nights • ALL-INCLUSIVE ✨ ✔️ 2 Adults + 2 Kids (12 & under) ✔️ No blackout dates — t" + }, + { + "id": "7612825138291166485", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7612825010587077908", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7612824883743001877", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7612824727228271892", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7612824588543626516", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7611713202807754005", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7611713028819537172", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7611712896271240469", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7611712751257292053", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7611712572877851925", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7611334215837256981", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7611334042528533780", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7611333870310460693", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7611333628567571732", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7611333495373204756", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7610599550029516053", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7610596527475608853", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7610593736338197780", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7610591010338458901", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7610586339880242453", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7610229016678796565", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7610225854345399573", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7610214868418956565", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7610212093278555413", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7606146061446417685", + "username": "travel.to.mexico8", + "title": "🚨 $299 MEXICO VACATION DEAL 🚨 🌴 5 Days / 4 Nights 🍹 All-Inclusive Resort 👨‍👩‍👧‍👦 2 Adults + 2 Kids 📅 NO blackout dates This deal wo" + }, + { + "id": "7606141425520053524", + "username": "travel.to.mexico8", + "title": "🚨 $299 MEXICO VACATION DEAL 🚨 🌴 5 Days / 4 Nights 🍹 All-Inclusive Resort 👨‍👩‍👧‍👦 2 Adults + 2 Kids 📅 NO blackout dates This deal wo" + }, + { + "id": "7606137412191882517", + "username": "travel.to.mexico8", + "title": "🚨 $299 MEXICO VACATION DEAL 🚨 🌴 5 Days / 4 Nights 🍹 All-Inclusive Resort 👨‍👩‍👧‍👦 2 Adults + 2 Kids 📅 NO blackout dates This deal wo" + }, + { + "id": "7606131787672079636", + "username": "travel.to.mexico8", + "title": "🚨 $299 MEXICO VACATION DEAL 🚨 🌴 5 Days / 4 Nights 🍹 All-Inclusive Resort 👨‍👩‍👧‍👦 2 Adults + 2 Kids 📅 NO blackout dates This deal wo" + }, + { + "id": "7606128084961545493", + "username": "travel.to.mexico8", + "title": "🚨 $299 MEXICO VACATION DEAL 🚨 🌴 5 Days / 4 Nights 🍹 All-Inclusive Resort 👨‍👩‍👧‍👦 2 Adults + 2 Kids 📅 NO blackout dates This deal wo" + }, + { + "id": "7605777886037003541", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7605774409177107732", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7605771533738380564", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7605765455831141653", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7605763423334337813", + "username": "travel.to.mexico8", + "title": "Picture this… Your kids laughing in the pool 🌊 You relaxing with a drink in hand 🍹 No stress. No planning headaches. Just paradise. 🏖️ 5 " + }, + { + "id": "7605403420501134613", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7605393075606850836", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7605388929088441621", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7605385078083980565", + "username": "travel.to.mexico8", + "title": "🌴 Escape to Paradise! 5 Days & 4 Nights in Mexico — All-Inclusive for Only $299! 🌞 Imagine sun-soaked beaches, crystal-clear waters, and u" + }, + { + "id": "7605034148218113301", + "username": "travel.to.mexico8", + "title": "Escape the everyday and treat your family to the vacation you’ve all been dreaming of — without breaking the bank! ✨ What’s Included: ✔️ 5 D" + }, + { + "id": "7605031715878341908", + "username": "travel.to.mexico8", + "title": "Escape the everyday and treat your family to the vacation you’ve all been dreaming of — without breaking the bank! ✨ What’s Included: ✔️ 5 D" + }, + { + "id": "7605025108616350996", + "username": "travel.to.mexico8", + "title": "Escape the everyday and treat your family to the vacation you’ve all been dreaming of — without breaking the bank! ✨ What’s Included: ✔️ 5 D" + }, + { + "id": "7605017071356792084", + "username": "travel.to.mexico8", + "title": "Escape the everyday and treat your family to the vacation you’ve all been dreaming of — without breaking the bank! ✨ What’s Included: ✔️ 5 D" + }, + { + "id": "7605011911742115093", + "username": "travel.to.mexico8", + "title": "Escape the everyday and treat your family to the vacation you’ve all been dreaming of — without breaking the bank! ✨ What’s Included: ✔️ 5 D" + }, + { + "id": "7603922127410056469", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7603917143503113492", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7603915141607853332", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7603904300644961556", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7603547156028476693", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7603542827678715156", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7602437874096639253", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7602435246679739668", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7602432094261841172", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7602427461330029844", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7602421070645169429", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7602416902236835093", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7601325166483737877", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7601319181895814421", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7601314019064040724", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7601307252854721813", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7601302133505264916", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7600953259527818517", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7600948478646242581", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7600928959915756820", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7600582098000383252", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7600564867724021013", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7600559301060578581", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7600213209684970772", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7600210082596539669", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7600207352259808532", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7600201175933177109", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7598734004359007509", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7598726765644696853", + "username": "travel.to.mexico8", + "title": "🌴 CANCUN GETAWAY for ONLY $299?! 😱✈️ Yes, you read that right! Treat your family to a 5 Days / 4 Nights All-Inclusive Vacation in beautifu" + }, + { + "id": "7598715055793868052", + "username": "travel.to.mexico8", + "title": "" + }, + { + "id": "7618035152551218450", + "username": "chris724santos", + "title": "They said Cancun isn’t safe anymore… meanwhile I’m here watching the sun on the beach, totally safe! Toll-Free (US & Canada): 1-888-602-2424" + }, + { + "id": "7618034303322148114", + "username": "chris724santos", + "title": "Everyone says Cancun is dangerous… I’m here right now and honestly… it’s one of the most peaceful vacations I’ve had Toll-Free (US & Canada)" + }, + { + "id": "7618032920892148999", + "username": "chris724santos", + "title": "New Yorkers, Let me show you what a real stress-free vacation looks like. In Cancun, all-inclusive means everything is covered—from food and" + }, + { + "id": "7618031655797738760", + "username": "chris724santos", + "title": "Calgarians, Close your eyes… now imagine the perfect vacation. Warm beaches, unlimited cocktails, all-you-can-eat dining, poolside service—y" + }, + { + "id": "7618030206959340818", + "username": "chris724santos", + "title": "Spoiler alert: Paradise is actually affordable. Cancun all-inclusive resorts bundle meals, drinks, pools, and beach access into one simple p" + }, + { + "id": "7616168374267055378", + "username": "chris724santos", + "title": "Calgarians, here’s something better than your morning coffee… Waking up in Cancun to ocean breezes, breakfast buffets, and unlimited resort " + }, + { + "id": "7616166257783606535", + "username": "chris724santos", + "title": "Easterners, warning: This might cause you to pack your bags. Cancun offers all-inclusive resorts with 24/7 food, drinks, entertainment, and " + }, + { + "id": "7616163461604609287", + "username": "chris724santos", + "title": "Ottawans, your future self will thank you for hearing this. A Cancun all-inclusive stay means zero planning, zero stress—just sunsets, warm " + }, + { + "id": "7616159717517167879", + "username": "chris724santos", + "title": "New Yorkers, stop scrolling if you need a break from reality. Imagine unlimited cocktails, poolside vibes, and white-sand beaches—all packag" + }, + { + "id": "7616157825328254215", + "username": "chris724santos", + "title": "Hey If you could be anywhere right now… why not Cancun? All-inclusive means unlimited meals, drinks, pools, and beach access—no extra planni" + }, + { + "id": "7615802426007801096", + "username": "chris724santos", + "title": "Americans and Canadians, ever wanted to feel like a VIP without paying VIP prices? Cancun’s all-inclusive packages give you luxury—beach acc" + }, + { + "id": "7615798501636672776", + "username": "chris724santos", + "title": "Canadians and US, this is your sign to take that vacation you’ve been putting off. Cancun all-inclusive resorts take care of everything: foo" + }, + { + "id": "7615795344672132360", + "username": "chris724santos", + "title": "Americans, cancel your weekend plans… because I just found something better. Picture white-sand beaches, unlimited cocktails, poolside servi" + }, + { + "id": "7615793545781267730", + "username": "chris724santos", + "title": "What if your next vacation didn’t require planning anything at all? In Cancun’s all-inclusive resorts, you get meals, drinks, activities, an" + }, + { + "id": "7615791481458117895", + "username": "chris724santos", + "title": "US and CA, Imagine waking up in paradise where your only job is to relax… From unlimited food & drinks to beachfront pools and daily enterta" + }, + { + "id": "7615454710916533511", + "username": "chris724santos", + "title": "Americans, Need a break? If you want sun + sand? Think Cancun! Cancún gives you luxury vibes without the price, super affordable! Message me" + }, + { + "id": "7615452362475392264", + "username": "chris724santos", + "title": "Canadians, If you’ve been thinking about taking a break, Cancún is the perfect place to make it happen. Beautiful beaches, warm weather, gre" + }, + { + "id": "7615450387285331208", + "username": "chris724santos", + "title": "Sometimes you don’t need a long plan — just the right destination. And for many, that’s Cancún. This promo covers a comfortable resort stay," + }, + { + "id": "7615448674025442567", + "username": "chris724santos", + "title": "Looking for a destination that’s worth every minute of your vacation time? Cancún is always a solid choice. You get beaches, culture, nightl" + }, + { + "id": "7615447194316885256", + "username": "chris724santos", + "title": "Picture yourself waking up to turquoise water and warm ocean breeze — that’s Cancún. This offer gives you resort access, pools, entertainmen" + }, + { + "id": "7615066189110185234", + "username": "chris724santos", + "title": "US and Canadians, Sometimes you just need a break from everything. And that break looks like a beachfront resort in Cancun, warm weather, an" + }, + { + "id": "7615063282382146824", + "username": "chris724santos", + "title": "Canadians and US, You just booked a trip to Cancun. You start imagining the beaches, the sunsets, the resort pools, and the feeling of final" + }, + { + "id": "7615061277148187922", + "username": "chris724santos", + "title": "US and Canadian, This is your sign to take a vacation. Cancun beaches, ocean views, and a few days away from the daily routine. Message me o" + }, + { + "id": "7615059773691546887", + "username": "chris724santos", + "title": "Hey, If someone offered you a 4-day resort stay in Cancun, would you take it? Because honestly… most people would say yes. DM or comment Can" + }, + { + "id": "7615058166895709458", + "username": "chris724santos", + "title": "US and Canadians, Imagine spending a few days in Cancun instead of being stuck at work. Honestly… everyone deserves a vacation like this at " + }, + { + "id": "7613982935712107794", + "username": "chris724santos", + "title": "US and Canadians, if you’ve never been to Cancun… this is your chance. You might want to check out these resort stays we still have availabl" + }, + { + "id": "7613978916574874887", + "username": "chris724santos", + "title": "Canadians and US, Stop scrolling if you love beach vacations. We currently have Cancun resort stays available. Send a message and I’ll expla" + }, + { + "id": "7613977546417310983", + "username": "chris724santos", + "title": "Cancun vacations are expensive… unless you know about this. US and Canadians, We currently have discounted resort stays available. Outro: “C" + }, + { + "id": "7613976159252008199", + "username": "chris724santos", + "title": "Canadians and US, If Cancun has been on your bucket list… this is your sign. We still have discounted stays at beachfront resorts in Cancun," + }, + { + "id": "7613974424663952658", + "username": "chris724santos", + "title": "US and Canadians, Planning a trip to Mexico this year? We have Cancun resort stays available for a limited time. Send me a quick message bef" + }, + { + "id": "7613613311300504850", + "username": "chris724santos", + "title": "Americans and Canadians… if you need a warm escape, listen to this. We’re offering a Cancun resort getaway with beautiful beaches and relaxi" + }, + { + "id": "7613611646375693576", + "username": "chris724santos", + "title": "Canadians and US, if Cancun is on your travel list, listen to this. We’re helping travelers enjoy resort stays near the beach. DM or comment" + }, + { + "id": "7613609941160529160", + "username": "chris724santos", + "title": "Hey, thinking about a tropical vacation this year? Cancun offers stunning beaches and relaxing resort experiences. Comment ‘INFO’ and I’ll m" + }, + { + "id": "7613608501083917576", + "username": "chris724santos", + "title": "US and Canadians, here’s a quick vacation idea for you. A relaxing Cancun resort stay where you can enjoy the beach and sunshine. Message me" + }, + { + "id": "7613607211008363784", + "username": "chris724santos", + "title": "Still deciding where to go for your next vacation? Cancun might be the perfect choice—tropical beaches and amazing resort stays. Comment ‘CA" + }, + { + "id": "7613185644747164935", + "username": "chris724santos", + "title": "Canadians and US, let’s turn your dream beach trip into reality! We have discounted Cancun packages for couples, families, or solo travelers" + }, + { + "id": "7613183865204641031", + "username": "chris724santos", + "title": "Hey! Planning a trip but don’t know where to start? We’ve got Cancun deals that make your vacation simple and affordable. Chat with me and I" + }, + { + "id": "7613182036366478610", + "username": "chris724santos", + "title": "Got 15 seconds? Let me show you your next vacation. Cancun—beautiful beaches, great food, and affordable promo stays. Message me now for the" + }, + { + "id": "7613180263983549703", + "username": "chris724santos", + "title": "US and Canadians, you deserve a break, don’t you? We've got amazing discounted stays in Cancun with flexible dates. Send me a quick message " + }, + { + "id": "7613178679929228552", + "username": "chris724santos", + "title": "Hey! When was the last time you treated yourself to a real vacation? Come enjoy Cancun’s beaches and top-notch resorts with our promo packag" + }, + { + "id": "7612818180699852050", + "username": "chris724santos", + "title": "Canadian and US travelers, need a warm reset? Cancun is calling your name. With an all-inclusive food, drinks, pools, and upgraded safety fo" + }, + { + "id": "7612815416146103570", + "username": "chris724santos", + "title": "Hi there US and Canadians, it’s the perfect time for a warm break—sun, sand, and all-inclusive comfort. Enjoy unlimited food and drinks, res" + }, + { + "id": "7612813070410337554", + "username": "chris724santos", + "title": "From the US and Canada to pure tropical bliss—your next getaway starts now. Everything is covered. Escape the cold and enjoy crystal-clear w" + }, + { + "id": "7612811446468005138", + "username": "chris724santos", + "title": "Hey Canadians and US, if you want beaches, luxury, and stress-free travel, this is it. Krystal Grand Cancun, includes full amenities, daily " + }, + { + "id": "7611704941241060626", + "username": "chris724santos", + "title": "Hey friends from Canada and US! Ready to make new memories? Cancún is one of the top beach destinations waiting for you. With an all-inclusi" + }, + { + "id": "7611702983197314311", + "username": "chris724santos", + "title": "Feeling stressed lately? Maybe it’s time to unwind in Cancún, where every day feels like summer. Our all-inclusive package covers food, drin" + }, + { + "id": "7611700828247706887", + "username": "chris724santos", + "title": "Hey US ans Canadians! Your next unforgettable vacation starts the moment you land in Cancún. Relax at an all-inclusive resort with unlimited" + }, + { + "id": "7611698597385522440", + "username": "chris724santos", + "title": "Ready for a warm escape? Your dream getaway starts in Cancún, one of the top beach destinations in Mexico. Stay in an all-inclusive resort w" + }, + { + "id": "7611694991936736530", + "username": "chris724santos", + "title": "Hey there! If you’ve been craving sunshine and turquoise waters, Cancún is calling your name. Enjoy a full all-inclusive experience—unlimite" + }, + { + "id": "7611334414362004743", + "username": "chris724santos", + "title": "US and Canadian travelers—looking for a safe destination with crystal-blue waters? Stay at an all-inclusive resort with unlimited food, unli" + }, + { + "id": "7611332584718568711", + "username": "chris724santos", + "title": "Hey, If you're flying from Canada or the US, Cancún is still one of the top safe zones for tourist vacations, especially inside the resort a" + }, + { + "id": "7611330525692841224", + "username": "chris724santos", + "title": "To my US and Canadian travelers—your next safe and sunny escape is waiting in Cancun! Enjoy stress-free all-inclusive perks: gourmet meals, " + }, + { + "id": "7611328364732845320", + "username": "chris724santos", + "title": "Whether you're coming from Canada or the US, you deserve a getaway! All-inclusive resorts here give you unlimited dining, open bars, pristin" + }, + { + "id": "7611326035031788818", + "username": "chris724santos", + "title": "Calling all travelers from the US and Canada - dreaming of a tropical escape. Imagine staying in an all-inclusive resort where your food, dr" + }, + { + "id": "7610608531678530823", + "username": "chris724santos", + "title": "Imagine staying right on the beach, with warm waters, full amenities, and restaurants just steps away. Shall we go ahead and confirm your sp" + }, + { + "id": "7610594867172920584", + "username": "chris724santos", + "title": "Hi! I’ll keep this quick—I’ve got a Cancun vacation offer that’s too good not to share with you. If this sounds good so far, we can lock it " + }, + { + "id": "7610582097748577544", + "username": "chris724santos", + "title": "Hi! Quick question—have you ever imagined waking up to crystal-blue beaches in Cancun? We’re talking about staying in a beachfront resort—wh" + }, + { + "id": "7610228483909782791", + "username": "chris724santos", + "title": "Cold front in Ontario? Heatwave in Arizona? Cancun stays perfect all year round. A luxury vacation doesn’t have to be expensive—especially i" + }, + { + "id": "7610226305556221191", + "username": "chris724santos", + "title": "Say yes to sun-soaked mornings and unforgettable nights in Cancun. Cancun is the escape you deserve. PM or comment CANCUN! Toll-Free (US & C" + }, + { + "id": "7610220494742211858", + "username": "chris724santos", + "title": "Whether you’re near the mountains of Alberta or the plains of Kansas, Cancun is your chance to switch scenery instantly. What are you waitin" + }, + { + "id": "7610214043680345352", + "username": "chris724santos", + "title": "From Montreal to California, travelers are picking Cancun for the beaches, the food, and the crystal-clear water. Pack light. Relax heavy. C" + }, + { + "id": "7610205552127266066", + "username": "chris724santos", + "title": "Canadian winters hitting hard? Ready to trade your routine for turquoise waters? Just pm or comment Cancun! ☀︎.⋆ 𖤓 Toll-Free (US & Canada)" + }, + { + "id": "7609117013834845447", + "username": "chris724santos", + "title": "Calgary folks, quick vacation notice! We still have discounted vacation package for you yall. Message me or comment CANCUN! ☀︎.⋆ 𖤓 Toll-Fr" + }, + { + "id": "7609115330388053256", + "username": "chris724santos", + "title": "Miami travelers, imagine a private balcony right inside your bedroom. These bedrooms? Pure hotel-goals. DM or message CANCUN now! 𖤓 。𖦹°‧ ⋆" + }, + { + "id": "7609113519203667218", + "username": "chris724santos", + "title": "Good day, Montreal people — listen to this. We’re helping families secure their discounted vacation this year. Feel free to message or comme" + }, + { + "id": "7609100427375725832", + "username": "chris724santos", + "title": "Hey California! Quick vacation heads-up! I have a vacation package promo still open for you! Message me or comment CANCUN right now to take " + }, + { + "id": "7609098109934767367", + "username": "chris724santos", + "title": "Toronto! Listen up, this one’s for you. Your promo slot is still open — just letting you know. Message me or comment CANCUN! °❀⋆.ೃ #TorontoT" + }, + { + "id": "7609095709073263880", + "username": "chris724santos", + "title": "Ottawa people, hear me real quick. We still have your promo rate available if you want to grab it. Feel free to DM or comment Cancun. #Cancu" + }, + { + "id": "7608741747623070983", + "username": "chris724santos", + "title": "No matter if you're in Florida or New York, I’m ready when you are. Travelers from any state can avail this! Just send me a quick message to" + }, + { + "id": "7608739596989451527", + "username": "chris724santos", + "title": "New Yorkers, if you need a break from the city, listen to this. Imagine a vacation that doesn’t break your budget. Let me know once you’re r" + }, + { + "id": "7608727936786648338", + "username": "chris724santos", + "title": "Stop searching—your next getaway might be right here. If you’re planning a trip soon, this might be exactly what you need. DM or comment Cab" + }, + { + "id": "7608725450415770888", + "username": "chris724santos", + "title": "If affordable luxury exists, this is it. A lot of people are asking about this. I’m just a message away whenever you’re ready to proceed. Co" + }, + { + "id": "7608360935576603922", + "username": "chris724santos", + "title": "If you’re thinking of Riviera Maya this year… watch this first. Look at what’s already included in the price. Slots are limited — secure you" + }, + { + "id": "7605030386606951687", + "username": "chris724santos", + "title": "Picture this—sitting by the shore in Cancun, watching the sky turn gold and pink 🌅 Mexico sunsets hit different! Wanna experience this, wit" + }, + { + "id": "7605027310227426567", + "username": "chris724santos", + "title": "Wait—before you scroll, imagine yourself in Riviera Maya for a few days… but without spending too much. We’ve got an all-inclusive vacation " + }, + { + "id": "7605014829023251719", + "username": "chris724santos", + "title": "Looking for a budget-friendly getaway? Puerto Vallarta got you. All-inclusive vacation. We work directly with the resorts, so you’ll get ver" + }, + { + "id": "7605011840698944786", + "username": "chris724santos", + "title": "POV: You’re finally taking that trip you’ve been putting off forever. 🌴✨ Imagine waking up to ocean views, unlimited food and drinks, and a" + }, + { + "id": "7605009292281859335", + "username": "chris724santos", + "title": "🌴 Hey! If you’ve been dreaming of a quick escape, Cancun is calling your name. I’ve got an all-inclusive vacation package you might love D" + }, + { + "id": "7603919277430721810", + "username": "chris724santos", + "title": "Cancun? With an all-inclusive deal? Yes, please. We’re offering a full package that covers everything. “Food, drinks, hotel, beach access — " + }, + { + "id": "7603916425044036882", + "username": "chris724santos", + "title": "What if your next reset moment looked like this Hello, Cabo Let me show you a peaceful, all-inclusive escape You get ocean views, pools, unl" + }, + { + "id": "7603907885462080775", + "username": "chris724santos", + "title": "STOP scrolling — Riviera Maya might be cheaper than your last weekend out. Serious. This all-inclusive package is actually doable. From whit" + }, + { + "id": "7603905329964895495", + "username": "chris724santos", + "title": "POV: You’re tired of adulting and Cancun is calling your name. So here’s a quick look at this all-inclusive escape. 5 days, 4 nights. Beachf" + }, + { + "id": "7603547720111901970", + "username": "chris724santos", + "title": "Ready for a break? Cancun, Mexico has everything you need for the perfect getaway! 🌴 All-inclusive means zero stress — eat, drink, relax, a" + }, + { + "id": "7603541240449223943", + "username": "chris724santos", + "title": "Escape the daily grind… Puerto Vallarta is your paradise! 🏖️ All-inclusive fun means you can relax and enjoy every moment — no planning req" + }, + { + "id": "7603533611102424328", + "username": "chris724santos", + "title": "Who’s ready for some sunshine? ☀️ Riviera Maya, Mexico is waiting! All your meals, drinks, and activities are included. Just show up and enj" + }, + { + "id": "7603529785150672146", + "username": "chris724santos", + "title": "Hey! Planning your next getaway? 🌴 Imagine yourself in Cancun, Mexico — sun, sand, and total relaxation all in one trip! This is an all-inc" + }, + { + "id": "7603176846171098375", + "username": "chris724santos", + "title": "⋆.˚ 𓇼 Cancun trip on a budget? Yup, possible. We have an all-inclusive Cancun vacation with a super good rate. Perfect for couples, familie" + }, + { + "id": "7603172717289950472", + "username": "chris724santos", + "title": "CABO 2026 — who’s ready? ⋆.˚ 𓇼 We have an all-inclusive Cabo vacation with one of the best resort experiences in Mexico. 5 days, 4 nights. " + }, + { + "id": "7603170263777201416", + "username": "chris724santos", + "title": "Looking for your next escape? Try Riviera Maya. ⋆.˚ 𓇼 We’re offering an all-inclusive vacation in Riviera Maya — one of the most beautiful " + }, + { + "id": "7603162462015245576", + "username": "chris724santos", + "title": "Stop waiting for the ‘perfect time’ — your Cancun trip could literally start here. I’m helping people get an all-inclusive Cancun vacation f" + }, + { + "id": "7603159313737190664", + "username": "chris724santos", + "title": "We have an all-inclusive Mexico vacation package perfect for couples, families, or friends. ⋆.˚ 𓇼 5 days, 4 nights. Unlimited food, unlimit" + }, + { + "id": "7603155425307184402", + "username": "chris724santos", + "title": "POV: You’re about to unlock a Cancun vacation you didn’t know was possible. Hey! If you’ve been dreaming of white-sand beaches, bottomless d" + }, + { + "id": "7602808291353201938", + "username": "chris724santos", + "title": "No plans this year? No problem! 𓇼 ⋆.˚ 𓆉 𓆝 𓆡 Cancun all-inclusive: sun, food, pools, fun — done! Just relax and let the vacation do its m" + }, + { + "id": "7602801434245074194", + "username": "chris724santos", + "title": "Cancun hack: do less, enjoy more. °𓇼🌊⋆🐚 Think beaches, fun, and nonstop sunshine Let’s make it happen! °𓇼🌊⋆🐚 Just show up and enjoy! #" + }, + { + "id": "7602797860169567506", + "username": "chris724santos", + "title": "Pack your bags… paradise awaits! 🌊⋆🐚🫧🥥🌴 Think turquoise waters, endless sun, and all-inclusive treats — cocktails, food, pools, and mor" + }, + { + "id": "7602793251241856263", + "username": "chris724santos", + "title": "Paradise isn’t a place… it’s Cancun! 𓆝 𓆡⋆.˚ 𓇼 Sun, fun, and zero worries… who’s in? 𓆝 𓆡⋆.˚ 𓇼 All-inclusive vibes, just show up and enj" + }, + { + "id": "7602788687109328135", + "username": "chris724santos", + "title": "Stress-free mode: ON. 🌺🌅🌊𓇼 ⋆. Let me show you how! Think all-inclusive means boring? Think again! Cancun has it all — stunning beaches, " + }, + { + "id": "7602783687364578567", + "username": "chris724santos", + "title": "Ever seen water THIS blue? 😍 Cancun is calling! 🐚🫧🥥🌴 Stay at an all-inclusive resort, sip cocktails by the pool, and enjoy endless sun " + }, + { + "id": "7602436296635632904", + "username": "chris724santos", + "title": "It’s the kind of trip where your only problem is choosing between the pool or the beach! 𓆉°❀⋆.ೃ࿔*:・ ˚⋆𓇼˚⊹ If Mexico is on your wishlist, I" + }, + { + "id": "7602432924708834568", + "username": "chris724santos", + "title": "Hold up! Look how relaxing this is! You literally wake up, eat, swim, relax, repeat! No planning needed! If you ever needed a sign to plan a" + }, + { + "id": "7602427669245889800", + "username": "chris724santos", + "title": "Let’s manifest good vibes today… starting with this view! 𓆉°❀⋆.ೃ࿔*:・ ˚⋆𓇼˚⊹ Perfect for couples, families, friends—just pure good vibes! 𓆉" + }, + { + "id": "7602422161747283207", + "username": "chris724santos", + "title": "Real talk… we all deserve a soft life moment, so check this out! Everything is inside the resort—food spots, bars, pools, activities. You li" + }, + { + "id": "7602416103356239111", + "username": "chris724santos", + "title": "If you need a sign to take a vacation… this is it! 𓆉°❀⋆.ೃ࿔*:・ ˚⋆𓇼˚⊹ Picture this: pool in the morning, beach in the afternoon, shows at ni" + }, + { + "id": "7602412252343438599", + "username": "chris724santos", + "title": "POV: You just need a break… so here’s Mexico. 𖤓 ⋆˚࿔⋆.˚ 𓇼 Mexico has insane all-inclusive resorts… unlimited food, unlimited drinks, unlimi" + }, + { + "id": "7601324989383527698", + "username": "chris724santos", + "title": "If travel heals you…🥥🌴🌺🌅🌊𖤓 。𖦹°‧ ⋆☀︎.⋆ 𖤓 ⋆˚࿔⋆.˚ 𓇼 SAME!!!! There’s something about Cancun that just refreshes your whole soul. Maybe" + }, + { + "id": "7601322386176134407", + "username": "chris724santos", + "title": "Imagine being here right now… 🌻🏝️🕶️👕🌴☀️ Quiet beaches, good food, and days that just flow. Cancun feels like a reset button. One day, y" + }, + { + "id": "7601315168517180679", + "username": "chris724santos", + "title": "If you’ve been craving a breather… ☀︎.⋆ 𖤓 ⋆˚࿔⋆.˚ 𓇼 Cancun is that place where time slows down and everything feels lighter. Adding this to" + }, + { + "id": "7601306996272270599", + "username": "chris724santos", + "title": "Hey, you deserve a break. 🥥🌴🌺🌅🌊 Let me bring you to Cancun Beaches, sunsets, and unlimited fun. If you want the package, just DM me “CA" + }, + { + "id": "7601301018957106440", + "username": "chris724santos", + "title": "Cancun? Yes please. 🌴 ₊✩‧₊˚౨ৎ˚₊✩‧₊ ˚⟡˖ All-inclusive vacation for you and your loved ones — food, drinks, activities, everything! ₊✩‧₊˚౨ৎ˚₊" + }, + { + "id": "7601298265182637320", + "username": "chris724santos", + "title": "Tired of the same routine? Take a break in Cancun! All-inclusive food, drinks, and 5 days of purefun. DM me “MEXICO” for the details! ✈️🔥 " + }, + { + "id": "7600953943153331463", + "username": "chris724santos", + "title": "Stop scrolling — this is your next vacation 😎🌅 Travel smart and save more with this all-inclusive deal 🔥✈️ Let’s book your Cancun trip to" + }, + { + "id": "7600949627860307208", + "username": "chris724santos", + "title": "Stop dreaming, start packing! Cancun awaits!! 😎🧳 Family-friendly + budget-friendly = perfect deal!! 🔥 Ready to make memories in Mexico? M" + }, + { + "id": "7600940014209371400", + "username": "chris724santos", + "title": "Who wants Cancun for 5 days? Stay, dine, swim, enjoy! Promo won’t last long — DM me now °❀⋆.ೃ࿔*:・ ˚ ༘ #724vacation #cancunmexico #alliinclus" + }, + { + "id": "7600936171354557704", + "username": "chris724santos", + "title": "All-inclusive package: food, drinks, pools, hotel… everything covered! DM me to start planning your 5D4N Mexico adventure! #724vacation #can" + }, + { + "id": "7600931800495475975", + "username": "chris724santos", + "title": "Getaway vacation!!! ALERT!!! Just pure vacation mode!!! Limited time offer!!! Secure yours now!!! #724vacation #cancunmexico #familyvacation" + }, + { + "id": "7600928850821860626", + "username": "chris724santos", + "title": "Sun, sand & unlimited drinks? Cancun is calling!!! #724vacation #cancunmexico #familyvacation #CapCut " + }, + { + "id": "7600582664717913351", + "username": "chris724santos", + "title": "Craving a beach escape? Mexico has white-sand beaches and amazing resorts waiting for you! Let’s plan your trip today! 🌸🌺🌝💥☀️ #724vacati" + }, + { + "id": "7600579435082812679", + "username": "chris724santos", + "title": "Ready for paradise? Let Mexico take your stress away. Let’s get you booked! 🍷🍾🍹☕️🫧🌸 #724vacation #cancunmexico #familyvacation #allincl" + }, + { + "id": "7600574564598385928", + "username": "chris724santos", + "title": "Looking for your next destination? Mexico has everything—beach, culture, food, and unforgettable moments. Your Mexican adventure starts here" + }, + { + "id": "7600568620103650578", + "username": "chris724santos", + "title": "Hey Travelers!!! Mexico is the best place to relax!!! DM Cancun!!! ☀️☀️ #724vacation #cancunmexico #alliinclusive " + }, + { + "id": "7600564652304518418", + "username": "chris724santos", + "title": "Need a break from stress? Fly to Mexico! Plan your trip today! DM Cancun! 🐚😎 #724vacation #cancunmexico #alliinclusive #familyvacation " + }, + { + "id": "7600559846118690055", + "username": "chris724santos", + "title": "Dreaming of a sunny escape? DM Cancun! ☀️🕶️ #cancunmexico #724vacation #alliinclusive " + }, + { + "id": "7600210549238009106", + "username": "chris724santos", + "title": "Cancun is waiting for you!!! DM meeee!!!! #cancunmexico #724vacation #alliinclusive " + }, + { + "id": "7600205520087043335", + "username": "chris724santos", + "title": "Want a luxury Cancun getaway? #cancunmexico #724vacation #familyvacation " + }, + { + "id": "7600194855343541522", + "username": "chris724santos", + "title": "Cancun is calling!!! ⛱️🐚 #cancunmexico #familyvacation #724vacation " + }, + { + "id": "7600189653106380039", + "username": "chris724santos", + "title": "Imagine yourself here in Cancun! 🐚 #cancunmexico #familyvacation #724vacation " + }, + { + "id": "7599836054589394194", + "username": "chris724santos", + "title": "Dreaming of a getaway? DM us Cancun #cancunmexico #724vacation #familyvacation " + }, + { + "id": "7599823703538453768", + "username": "chris724santos", + "title": "All Inclusive Vacation, DM us CANCUN #cancunmexico #724vacation #familyvacation " + }, + { + "id": "7599817155001044231", + "username": "chris724santos", + "title": "Cancun Vacation getaway #cancunmexico #724vacation #allinclusive " + } +] \ No newline at end of file diff --git a/src/hooks/useEarlyLead.ts b/src/hooks/useEarlyLead.ts new file mode 100644 index 0000000..903978d --- /dev/null +++ b/src/hooks/useEarlyLead.ts @@ -0,0 +1,76 @@ +'use client' + +import { useCallback, useEffect, useRef, useState } from 'react' + +const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + +interface CapturePayload { + email: string + phone?: string + name?: string + source_lp?: string + referral_code?: string + utm_source?: string + utm_medium?: string + utm_campaign?: string +} + +interface UseEarlyLeadOptions { + debounceMs?: number +} + +/** + * Fire-and-forget early lead capture: posts to /api/track/lead the moment a + * valid email is detected, again whenever the payload changes (debounced). + * Used so we never lose a lead to abandoned-cart / abandoned-form behavior. + * + * Returns { captured, capture } — `captured` flips to true once the server + * has acknowledged at least one valid payload; `capture` is the manual + * trigger (call from onBlur for instant-on-blur capture in addition to the + * debounced auto-trigger). + */ +export function useEarlyLead(payload: CapturePayload, options: UseEarlyLeadOptions = {}) { + const { debounceMs = 800 } = options + const [captured, setCaptured] = useState(false) + const lastSentRef = useRef('') + const inFlightRef = useRef(false) + const timerRef = useRef | null>(null) + + const send = useCallback(async (p: CapturePayload) => { + if (!p.email || !EMAIL_RE.test(p.email)) return + const key = JSON.stringify(p) + if (key === lastSentRef.current || inFlightRef.current) return + inFlightRef.current = true + try { + const res = await fetch('/api/track/lead', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(p), + keepalive: true, + }) + if (res.ok) { + lastSentRef.current = key + setCaptured(true) + // Remember the email so the checkout can prefill it (no re-asking). + try { localStorage.setItem('hi2b_email', p.email) } catch { /* ignore */ } + } + } catch { + // Silent — capture is best-effort + } finally { + inFlightRef.current = false + } + }, []) + + const payloadKey = JSON.stringify(payload) + + useEffect(() => { + if (timerRef.current) clearTimeout(timerRef.current) + timerRef.current = setTimeout(() => { void send(payload) }, debounceMs) + return () => { if (timerRef.current) clearTimeout(timerRef.current) } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [payloadKey, debounceMs]) + + const capture = useCallback(() => { void send(payload) }, [payloadKey, send]) // eslint-disable-line react-hooks/exhaustive-deps + + return { captured, capture } +} diff --git a/src/hooks/useTrackingParams.ts b/src/hooks/useTrackingParams.ts new file mode 100644 index 0000000..db35de4 --- /dev/null +++ b/src/hooks/useTrackingParams.ts @@ -0,0 +1,57 @@ +'use client' + +import { useEffect, useState } from 'react' + +export interface TrackingParams { + ref?: string + utm_source?: string + utm_medium?: string + utm_campaign?: string + source_lp?: string +} + +const STORAGE_KEY = 'hi2b_tracking' + +export function useTrackingParams(sourceLp?: string): TrackingParams { + const [params, setParams] = useState({}) + + useEffect(() => { + // Read from URL + const url = new URL(window.location.href) + const ref = url.searchParams.get('ref') || undefined + const utm_source = url.searchParams.get('utm_source') || undefined + const utm_medium = url.searchParams.get('utm_medium') || undefined + const utm_campaign = url.searchParams.get('utm_campaign') || undefined + + // Merge with sessionStorage (URL params take priority) + const stored = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}') + const merged: TrackingParams = { + ref: ref || stored.ref, + utm_source: utm_source || stored.utm_source, + utm_medium: utm_medium || stored.utm_medium, + utm_campaign: utm_campaign || stored.utm_campaign, + source_lp: sourceLp || stored.source_lp, + } + + // Persist + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(merged)) + setParams(merged) + + // Record page view + if (sourceLp) { + fetch('/api/track/pageview', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + page_slug: sourceLp, + referral_code: merged.ref, + utm_source: merged.utm_source, + utm_medium: merged.utm_medium, + utm_campaign: merged.utm_campaign, + }), + }).catch(() => {}) + } + }, [sourceLp]) + + return params +} diff --git a/src/lib/admin-auth.ts b/src/lib/admin-auth.ts new file mode 100644 index 0000000..43b883a --- /dev/null +++ b/src/lib/admin-auth.ts @@ -0,0 +1,72 @@ +import bcrypt from 'bcryptjs' +import jwt from 'jsonwebtoken' +import { cookies } from 'next/headers' +import { randomBytes, createHash } from 'crypto' + +if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 32) { + throw new Error('JWT_SECRET env var is required (≥32 chars)') +} +const BASE_SECRET: string = process.env.JWT_SECRET +const ADMIN_SECRET = createHash('sha256').update(BASE_SECRET + '::admin').digest('hex') +const AFFILIATE_SECRET = createHash('sha256').update(BASE_SECRET + '::affiliate').digest('hex') + +// ─── Admin Auth ──────────────────────────────────────────── + +export function createAdminToken(payload: { id: number; email: string; role: string }): string { + return jwt.sign({ ...payload, type: 'admin' }, ADMIN_SECRET, { expiresIn: '24h' }) +} + +export function verifyAdminToken(token: string): { id: number; email: string; role: string } | null { + try { + const decoded = jwt.verify(token, ADMIN_SECRET) as any + if (decoded.type !== 'admin') return null + return decoded + } catch { return null } +} + +export async function getAdminSession() { + const cookieStore = await cookies() + const token = cookieStore.get('admin_token')?.value + if (!token) return null + return verifyAdminToken(token) +} + +// ─── Affiliate Auth ──────────────────────────────────────── + +export function createAffiliateToken(payload: { id: number; email: string; referral_code: string }): string { + return jwt.sign({ ...payload, type: 'affiliate' }, AFFILIATE_SECRET, { expiresIn: '7d' }) +} + +export function verifyAffiliateToken(token: string): { id: number; email: string; referral_code: string } | null { + try { + const decoded = jwt.verify(token, AFFILIATE_SECRET) as any + if (decoded.type !== 'affiliate') return null + return decoded + } catch { return null } +} + +export async function getAffiliateSession() { + const cookieStore = await cookies() + const token = cookieStore.get('affiliate_token')?.value + if (!token) return null + return verifyAffiliateToken(token) +} + +// ─── Shared ──────────────────────────────────────────────── + +export async function hashPassword(password: string): Promise { + return bcrypt.hash(password, 12) +} + +export async function verifyPassword(password: string, hash: string): Promise { + return bcrypt.compare(password, hash) +} + +export function generateReferralCode(name: string): string { + const prefix = name.replace(/[^a-zA-Z]/g, '').substring(0, 3).toUpperCase() || 'AFF' + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' + const bytes = randomBytes(4) + let suffix = '' + for (let i = 0; i < 4; i++) suffix += chars[bytes[i] % chars.length] + return `${prefix}${suffix}` +} diff --git a/src/lib/auth-utils.ts b/src/lib/auth-utils.ts new file mode 100644 index 0000000..78a68e0 --- /dev/null +++ b/src/lib/auth-utils.ts @@ -0,0 +1,48 @@ +import bcrypt from 'bcryptjs' +import jwt from 'jsonwebtoken' +import { cookies } from 'next/headers' +import { randomBytes } from 'crypto' + +if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 32) { + throw new Error('JWT_SECRET env var is required (≥32 chars)') +} +const JWT_SECRET: string = process.env.JWT_SECRET +const TOKEN_EXPIRY = '7d' + +export async function hashPassword(password: string): Promise { + return bcrypt.hash(password, 12) +} + +export async function verifyPassword(password: string, hash: string): Promise { + return bcrypt.compare(password, hash) +} + +export function createToken(payload: { id: number; email: string }): string { + return jwt.sign(payload, JWT_SECRET, { expiresIn: TOKEN_EXPIRY }) +} + +export function verifyToken(token: string): { id: number; email: string } | null { + try { + return jwt.verify(token, JWT_SECRET) as { id: number; email: string } + } catch { + return null + } +} + +export async function getSessionUser(): Promise<{ id: number; email: string } | null> { + const cookieStore = await cookies() + const token = cookieStore.get('auth_token')?.value + if (!token) return null + return verifyToken(token) +} + +export function generateResetToken(): string { + return randomBytes(32).toString('hex') +} + +export function generateCertificateNumber(): string { + const prefix = 'MPV' + const year = new Date().getFullYear() + const random = randomBytes(4).toString('hex').toUpperCase().slice(0, 6) + return `${prefix}-${year}-${random}` +} diff --git a/src/lib/db-admin.ts b/src/lib/db-admin.ts new file mode 100644 index 0000000..d351b1d --- /dev/null +++ b/src/lib/db-admin.ts @@ -0,0 +1,197 @@ +import pool from './db-mysql' + +// ─── Admin Stats ─────────────────────────────────────────── + +export async function getKPIStats() { + const [salesRow] = await pool.execute( + `SELECT COUNT(*) as total_sales, COALESCE(SUM(amount),0) as total_revenue + FROM signups WHERE payment_status IN ('active','completed')` + ) as any[] + + const [mrrRow] = await pool.execute( + `SELECT COUNT(*) * 29 as mrr FROM signups WHERE payment_status = 'active' AND payment_plan_months > 1` + ) as any[] + + const [leadsRow] = await pool.execute(`SELECT COUNT(*) as total_leads FROM ebook_leads`) as any[] + + const [viewsRow] = await pool.execute(`SELECT COUNT(*) as total_views FROM page_views`) as any[] + + const totalSales = salesRow[0]?.total_sales || 0 + const totalLeads = leadsRow[0]?.total_leads || 0 + const conversionRate = totalLeads > 0 ? ((totalSales / totalLeads) * 100).toFixed(1) : '0' + + return { + totalSales: salesRow[0]?.total_sales || 0, + totalRevenue: parseFloat(salesRow[0]?.total_revenue || 0), + mrr: mrrRow[0]?.mrr || 0, + totalLeads, + totalViews: viewsRow[0]?.total_views || 0, + conversionRate: parseFloat(conversionRate), + } +} + +// ─── Sales by LP ─────────────────────────────────────────── + +export async function getSalesByLP() { + const [rows] = await pool.execute( + `SELECT COALESCE(source_lp,'unknown') as name, COUNT(*) as count, COALESCE(SUM(amount),0) as revenue + FROM signups WHERE payment_status IN ('active','completed') + GROUP BY source_lp ORDER BY count DESC LIMIT 20` + ) as any[] + return rows +} + +// ─── Sales by Source ─────────────────────────────────────── + +export async function getSalesBySource() { + const [rows] = await pool.execute( + `SELECT COALESCE(utm_source,'direct') as name, COUNT(*) as count, COALESCE(SUM(amount),0) as revenue + FROM signups WHERE payment_status IN ('active','completed') + GROUP BY utm_source ORDER BY count DESC` + ) as any[] + return rows +} + +// ─── Sales by Affiliate ─────────────────────────────────── + +export async function getSalesByAffiliate() { + const [rows] = await pool.execute( + `SELECT a.name, a.referral_code, COUNT(ar.id) as sales, COALESCE(SUM(ar.commission_amount),0) as total_commission + FROM affiliates a + LEFT JOIN affiliate_referrals ar ON a.id = ar.affiliate_id + GROUP BY a.id ORDER BY sales DESC` + ) as any[] + return rows +} + +// ─── Revenue Over Time ───────────────────────────────────── + +export async function getRevenueOverTime(days: number = 30) { + const [rows] = await pool.execute( + `SELECT DATE(created_at) as date, COUNT(*) as sales, COALESCE(SUM(amount),0) as revenue + FROM signups WHERE payment_status IN ('active','completed') AND created_at >= DATE_SUB(NOW(), INTERVAL ? DAY) + GROUP BY DATE(created_at) ORDER BY date`, + [days] + ) as any[] + return rows +} + +// ─── Funnel Data ─────────────────────────────────────────── + +export async function getFunnelData(days: number = 30) { + const since = `DATE_SUB(NOW(), INTERVAL ${days} DAY)` + + const [views] = await pool.execute(`SELECT COUNT(*) as c FROM page_views WHERE created_at >= ${since}`) as any[] + const [leads] = await pool.execute(`SELECT COUNT(*) as c FROM ebook_leads WHERE created_at >= ${since}`) as any[] + const [signups] = await pool.execute(`SELECT COUNT(*) as c FROM signups WHERE created_at >= ${since}`) as any[] + const [paid] = await pool.execute(`SELECT COUNT(*) as c FROM signups WHERE payment_status IN ('active','completed') AND created_at >= ${since}`) as any[] + + return { + pageViews: views[0]?.c || 0, + ebookDownloads: leads[0]?.c || 0, + signups: signups[0]?.c || 0, + paidCustomers: paid[0]?.c || 0, + } +} + +// ─── Paginated Sales ─────────────────────────────────────── + +export async function getSales(params: { + page?: number + limit?: number + status?: string + source_lp?: string + affiliate_id?: number + utm_source?: string + search?: string +}) { + const { page = 1, limit = 25, status, source_lp, affiliate_id, utm_source, search } = params + const offset = (page - 1) * limit + const conditions: string[] = [] + const values: any[] = [] + + if (status) { conditions.push('s.payment_status = ?'); values.push(status) } + if (source_lp) { conditions.push('s.source_lp = ?'); values.push(source_lp) } + if (affiliate_id) { conditions.push('s.affiliate_id = ?'); values.push(affiliate_id) } + if (utm_source) { conditions.push('s.utm_source = ?'); values.push(utm_source) } + if (search) { conditions.push('(s.email LIKE ? OR s.full_name LIKE ?)'); values.push(`%${search}%`, `%${search}%`) } + + const where = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '' + + const [countRow] = await pool.execute(`SELECT COUNT(*) as total FROM signups s ${where}`, values) as any[] + const total = countRow[0]?.total || 0 + + const [rows] = await pool.execute( + `SELECT s.*, + a.name as affiliate_name, + a.referral_code as aff_code, + (SELECT COUNT(*) FROM payments p + WHERE p.signup_id = s.id AND p.status = 'completed') as payments_made + FROM signups s LEFT JOIN affiliates a ON s.affiliate_id = a.id + ${where} ORDER BY s.created_at DESC LIMIT ? OFFSET ?`, + [...values, limit, offset] + ) as any[] + + return { data: rows, total, page, limit, totalPages: Math.ceil(total / limit) } +} + +// ─── Paginated Leads ─────────────────────────────────────── + +export async function getLeads(params: { page?: number; limit?: number; search?: string }) { + const { page = 1, limit = 25, search } = params + const offset = (page - 1) * limit + const where = search ? 'WHERE email LIKE ? OR name LIKE ?' : '' + const values = search ? [`%${search}%`, `%${search}%`] : [] + + const [countRow] = await pool.execute(`SELECT COUNT(*) as total FROM ebook_leads ${where}`, values) as any[] + const [rows] = await pool.execute( + `SELECT * FROM ebook_leads ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, + [...values, limit, offset] + ) as any[] + + return { data: rows, total: countRow[0]?.total || 0, page, limit } +} + +// ─── Affiliate Management ────────────────────────────────── + +export async function getAffiliates() { + const [rows] = await pool.execute( + `SELECT a.*, COUNT(ar.id) as referral_count, COALESCE(SUM(CASE WHEN ar.status != 'rejected' THEN ar.commission_amount ELSE 0 END),0) as earned + FROM affiliates a LEFT JOIN affiliate_referrals ar ON a.id = ar.affiliate_id + GROUP BY a.id ORDER BY a.created_at DESC` + ) as any[] + return rows +} + +export async function updateAffiliateStatus(id: number, status: string) { + await pool.execute('UPDATE affiliates SET status = ? WHERE id = ?', [status, id]) +} + +// ─── Payouts ─────────────────────────────────────────────── + +export async function getPayouts() { + const [rows] = await pool.execute( + `SELECT ap.*, a.name as affiliate_name, a.email as affiliate_email + FROM affiliate_payouts ap JOIN affiliates a ON ap.affiliate_id = a.id + ORDER BY ap.created_at DESC` + ) as any[] + return rows +} + +export async function createPayout(affiliateId: number, amount: number, method: string, reference: string) { + const [result] = await pool.execute( + `INSERT INTO affiliate_payouts (affiliate_id, amount, method, reference, status) VALUES (?, ?, ?, ?, 'completed')`, + [affiliateId, amount, method, reference] + ) as any[] + + // Update affiliate totals + await pool.execute('UPDATE affiliates SET total_paid = total_paid + ? WHERE id = ?', [amount, affiliateId]) + + // Mark referrals as paid + await pool.execute( + `UPDATE affiliate_referrals SET status = 'paid', paid_at = NOW() WHERE affiliate_id = ? AND status = 'approved'`, + [affiliateId] + ) + + return (result as any).insertId +} diff --git a/src/lib/db-mysql.ts b/src/lib/db-mysql.ts new file mode 100644 index 0000000..682ad08 --- /dev/null +++ b/src/lib/db-mysql.ts @@ -0,0 +1,296 @@ +import mysql from 'mysql2/promise' + +const { MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE } = process.env +if (!MYSQL_HOST || !MYSQL_USER || !MYSQL_PASSWORD || !MYSQL_DATABASE) { + throw new Error('MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, and MYSQL_DATABASE env vars are required') +} + +const pool = mysql.createPool({ + host: MYSQL_HOST, + user: MYSQL_USER, + password: MYSQL_PASSWORD, + database: MYSQL_DATABASE, + waitForConnections: true, + connectionLimit: 10, +}) + +export default pool + +// ─── Signups ─────────────────────────────────────────────── + +export async function createSignup(data: { + email: string + full_name?: string | null + phone?: string | null + amount?: number | null + monthly_payment?: number | null + payment_plan_months?: number | null + payment_status?: string | null + source_lp?: string + referral_code?: string + utm_source?: string + utm_medium?: string + utm_campaign?: string +}) { + // Apply sensible defaults so the client dashboard never crashes on nulls. + const full_name = data.full_name ?? '' + const phone = data.phone ?? '' + const amount = data.amount ?? 290 + const monthly_payment = data.monthly_payment ?? 29 + const payment_plan_months = data.payment_plan_months ?? 10 + const payment_status = data.payment_status ?? 'pending' + + const [existing] = await pool.execute('SELECT * FROM signups WHERE email = ?', [data.email]) as any[] + + if (existing.length > 0) { + await pool.execute( + `UPDATE signups SET payment_status = ?, full_name = ?, phone = ?, amount = ?, monthly_payment = ?, + payment_plan_months = ?, source_lp = COALESCE(?, source_lp), referral_code = COALESCE(?, referral_code), + utm_source = COALESCE(?, utm_source), utm_medium = COALESCE(?, utm_medium), utm_campaign = COALESCE(?, utm_campaign) + WHERE email = ?`, + [payment_status, full_name, phone, amount, monthly_payment, payment_plan_months, + data.source_lp || null, data.referral_code || null, + data.utm_source || null, data.utm_medium || null, data.utm_campaign || null, data.email] + ) + // Re-fetch to get the ID + const [updated] = await pool.execute('SELECT * FROM signups WHERE email = ?', [data.email]) as any[] + return { error: null, data: updated[0] } + } + + // Look up affiliate by referral code + let affiliateId: number | null = null + if (data.referral_code) { + const [aff] = await pool.execute('SELECT id FROM affiliates WHERE referral_code = ? AND status = ?', [data.referral_code, 'active']) as any[] + if (aff.length > 0) affiliateId = aff[0].id + } + + const [result] = await pool.execute( + `INSERT INTO signups (email, full_name, phone, amount, monthly_payment, payment_plan_months, payment_status, + source_lp, affiliate_id, referral_code, utm_source, utm_medium, utm_campaign) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [data.email, full_name, phone, amount, monthly_payment, payment_plan_months, payment_status, + data.source_lp || null, affiliateId, data.referral_code || null, + data.utm_source || null, data.utm_medium || null, data.utm_campaign || null] + ) as any[] + + const [rows] = await pool.execute('SELECT * FROM signups WHERE id = ?', [(result as any).insertId]) as any[] + return { error: null, data: rows[0] } +} + +// Strict whitelist of columns that updateSignup is allowed to modify. Any key +// outside this set is rejected to prevent SQL injection via attacker-controlled +// object keys. +const UPDATE_SIGNUP_ALLOWED_COLUMNS = new Set([ + 'full_name', 'phone', 'amount', 'monthly_payment', 'payment_plan_months', + 'payment_status', 'certificate_number', 'certificate_expires', 'subscription_id', + 'source_lp', 'affiliate_id', 'referral_code', 'utm_source', 'utm_medium', + 'utm_campaign', 'password_hash', 'reset_token', 'reset_token_expires', 'destination', +]) + +export async function updateSignup(id: number, updates: Record) { + const keys = Object.keys(updates) + for (const key of keys) { + if (!UPDATE_SIGNUP_ALLOWED_COLUMNS.has(key)) { + throw new Error(`updateSignup: disallowed column "${key}"`) + } + } + if (keys.length === 0) return + const fields = keys.map(k => `${k} = ?`).join(', ') + const values = keys.map(k => updates[k]) + await pool.execute(`UPDATE signups SET ${fields} WHERE id = ?`, [...values, id]) +} + +// ─── Payments ────────────────────────────────────────────── + +export async function createPayment(data: { + signup_id: number + payment_method?: string + amount: number + currency?: string + payment_type?: string + status?: string + transaction_id?: string +}) { + const [result] = await pool.execute( + `INSERT INTO payments (signup_id, payment_method, amount, currency, payment_type, status, transaction_id) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [data.signup_id, data.payment_method || 'nmi', data.amount, data.currency || 'USD', + data.payment_type || 'initial', data.status || 'completed', data.transaction_id || null] + ) as any[] + return { insertId: (result as any).insertId } +} + +// ─── Ebook Leads ─────────────────────────────────────────── + +export async function createEbookLead(data: { + email: string + name?: string + source_lp?: string + utm_source?: string + utm_medium?: string + utm_campaign?: string +}) { + try { + await pool.execute( + `INSERT INTO ebook_leads (email, name, source_lp, utm_source, utm_medium, utm_campaign) VALUES (?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE source_lp = COALESCE(VALUES(source_lp), source_lp), + utm_source = COALESCE(VALUES(utm_source), utm_source)`, + [data.email, data.name || null, data.source_lp || 'unknown', + data.utm_source || null, data.utm_medium || null, data.utm_campaign || null] + ) + return { error: null } + } catch (err) { + return { error: err } + } +} + +// ─── Early Leads (debounced input capture, pre-submit) ───── + +let earlyLeadsTableReady: Promise | null = null +function ensureEarlyLeadsTable(): Promise { + if (!earlyLeadsTableReady) { + earlyLeadsTableReady = pool.execute(` + CREATE TABLE IF NOT EXISTS early_leads ( + id INT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + phone VARCHAR(50), + name VARCHAR(255), + ip_address VARCHAR(64), + source_lp VARCHAR(100), + referral_code VARCHAR(50), + utm_source VARCHAR(100), + utm_medium VARCHAR(100), + utm_campaign VARCHAR(100), + confirmed TINYINT(1) NOT NULL DEFAULT 0, + confirmed_at TIMESTAMP NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_confirmed (confirmed), + INDEX idx_created (created_at) + ) + `).then(() => undefined) + } + return earlyLeadsTableReady +} + +export async function upsertEarlyLead(data: { + email: string + phone?: string | null + name?: string | null + ip_address?: string | null + source_lp?: string | null + referral_code?: string | null + utm_source?: string | null + utm_medium?: string | null + utm_campaign?: string | null +}) { + await ensureEarlyLeadsTable() + await pool.execute( + `INSERT INTO early_leads + (email, phone, name, ip_address, source_lp, referral_code, utm_source, utm_medium, utm_campaign) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + phone = COALESCE(VALUES(phone), phone), + name = COALESCE(VALUES(name), name), + ip_address = COALESCE(VALUES(ip_address), ip_address), + source_lp = COALESCE(VALUES(source_lp), source_lp), + referral_code = COALESCE(VALUES(referral_code), referral_code), + utm_source = COALESCE(VALUES(utm_source), utm_source), + utm_medium = COALESCE(VALUES(utm_medium), utm_medium), + utm_campaign = COALESCE(VALUES(utm_campaign), utm_campaign)`, + [data.email, data.phone || null, data.name || null, data.ip_address || null, + data.source_lp || null, data.referral_code || null, + data.utm_source || null, data.utm_medium || null, data.utm_campaign || null] + ) +} + +export async function confirmEarlyLead(email: string) { + await ensureEarlyLeadsTable() + await pool.execute( + 'UPDATE early_leads SET confirmed = 1, confirmed_at = CURRENT_TIMESTAMP WHERE email = ?', + [email] + ) +} + +// ─── Affiliate Short Codes (2-char vanity for hi2b.com/pay/XX) ─ + +let affiliateShortCodeReady: Promise | null = null +function ensureAffiliateShortCodeColumn(): Promise { + if (!affiliateShortCodeReady) { + affiliateShortCodeReady = (async () => { + // Add column if missing — MySQL has no IF NOT EXISTS for ADD COLUMN, so catch dup error + try { + await pool.execute('ALTER TABLE affiliates ADD COLUMN short_code VARCHAR(8) UNIQUE') + } catch (e: any) { + if (!String(e?.code || e?.message).match(/Duplicate column|already exists/i)) throw e + } + })() + } + return affiliateShortCodeReady +} + +// Confusable-free alphabet for 2-char codes (32 chars = 1024 combinations) +const SC_ALPHA = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' + +async function generateUniqueShortCode(): Promise { + await ensureAffiliateShortCodeColumn() + for (let attempt = 0; attempt < 50; attempt++) { + const code = SC_ALPHA[Math.floor(Math.random() * SC_ALPHA.length)] + + SC_ALPHA[Math.floor(Math.random() * SC_ALPHA.length)] + const [rows] = await pool.execute('SELECT id FROM affiliates WHERE short_code = ?', [code]) as any[] + if (rows.length === 0) return code + } + throw new Error('No free short codes — increase to 3 chars') +} + +export async function ensureAffiliateShortCode(affiliateId: number): Promise { + await ensureAffiliateShortCodeColumn() + const [rows] = await pool.execute('SELECT short_code FROM affiliates WHERE id = ?', [affiliateId]) as any[] + if (rows[0]?.short_code) return rows[0].short_code + const code = await generateUniqueShortCode() + await pool.execute('UPDATE affiliates SET short_code = ? WHERE id = ?', [code, affiliateId]) + return code +} + +export async function getAffiliateByShortCode(code: string): Promise<{ id: number; referral_code: string; name: string } | null> { + await ensureAffiliateShortCodeColumn() + const [rows] = await pool.execute( + 'SELECT id, referral_code, name FROM affiliates WHERE short_code = ? AND status = ?', + [code.toUpperCase(), 'active'] + ) as any[] + return rows.length > 0 ? rows[0] : null +} + +// ─── Affiliate Referral (called after successful payment) ── + +export async function createAffiliateReferral(signupId: number) { + // Get signup with affiliate info + const [signups] = await pool.execute('SELECT * FROM signups WHERE id = ?', [signupId]) as any[] + if (signups.length === 0) return + + const signup = signups[0] + if (!signup.affiliate_id) return + + // Get affiliate commission rate + const [affs] = await pool.execute('SELECT * FROM affiliates WHERE id = ? AND status = ?', [signup.affiliate_id, 'active']) as any[] + if (affs.length === 0) return + + const affiliate = affs[0] + const commissionAmount = (signup.monthly_payment || 29) * (affiliate.commission_rate / 100) + + // Check for duplicate + const [existing] = await pool.execute( + 'SELECT id FROM affiliate_referrals WHERE affiliate_id = ? AND signup_id = ?', + [affiliate.id, signupId] + ) as any[] + if (existing.length > 0) return + + // Create referral + await pool.execute( + `INSERT INTO affiliate_referrals (affiliate_id, signup_id, commission_amount, status) VALUES (?, ?, ?, 'pending')`, + [affiliate.id, signupId, commissionAmount] + ) + + // Update affiliate total_earned + await pool.execute('UPDATE affiliates SET total_earned = total_earned + ? WHERE id = ?', [commissionAmount, affiliate.id]) +} diff --git a/src/lib/email.ts b/src/lib/email.ts new file mode 100644 index 0000000..260c7c8 --- /dev/null +++ b/src/lib/email.ts @@ -0,0 +1,414 @@ +const MAIL_API_URL = process.env.MAIL_API_URL || 'https://mail.3ava.com/api/emails' +const MAIL_API_KEY = process.env.MAIL_API_KEY +const FROM = process.env.EMAIL_FROM || 'Mexico Paradise Vacations ' +const REPLY_TO = process.env.EMAIL_REPLY_TO || 'support@724vacation.com' +const APP_URL = process.env.NEXT_PUBLIC_APP_URL || 'https://hi2b.com' + +const HEADER = ` +
+

Mexico Paradise Vacations

+

Your All-Inclusive Paradise Awaits

+
` + +const FOOTER = ` +
+

Mexico Paradise Vacations • hi2b.com

+

Toll-Free: 888-602-2424

+

Mon-Fri 9am-8pm • Sat 10am-4pm EST

+
` + +function wrap(content: string) { + return ` + + + + +
+ ${HEADER} +
+ ${content} +
+ ${FOOTER} +
+ +` +} + +function btn(text: string, url: string, color = '#E8651A') { + return `
+ ${text} +
` +} + +interface SendEmailInput { + to: string | string[] + subject: string + html: string + text?: string + from?: string + replyTo?: string +} + +/** + * Send a transactional email via 3AVA Mail. + * Never throws into the request path — logs and resolves with the API response or null. + */ +async function sendMail(input: SendEmailInput): Promise<{ id?: string; status?: string } | null> { + if (!MAIL_API_KEY) { + console.error('[mail] MAIL_API_KEY not configured; skipping send to', input.to) + return null + } + const replyTo = input.replyTo ?? REPLY_TO + const body = { + from: input.from ?? FROM, + to: Array.isArray(input.to) ? input.to : [input.to], + subject: input.subject, + html: input.html, + ...(input.text ? { text: input.text } : {}), + ...(replyTo ? { reply_to: [replyTo] } : {}), + } + try { + const res = await fetch(MAIL_API_URL, { + method: 'POST', + headers: { + Authorization: `Bearer ${MAIL_API_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }) + const data = await res.json().catch(() => ({})) + if (!res.ok) { + console.error('[mail] send failed', res.status, data) + return null + } + return data + } catch (err) { + console.error('[mail] network error', err) + return null + } +} + +// ─── Welcome / Payment Confirmation ─────────────────────── + +export async function sendWelcomeEmail(email: string, name: string, certificateNumber: string, paymentType: string, amount: number) { + return sendMail({ + to: email, + subject: 'Welcome to Paradise! Your Vacation Certificate is Ready', + html: wrap(` +

Welcome to Paradise, ${name}!

+

Congratulations! Your vacation certificate has been activated. Here are your details:

+ +
+

Certificate Number

+

${certificateNumber}

+
+ +
+

Payment: $${amount} ${paymentType === 'monthly' ? '(first of 10 monthly payments)' : '(one-time payment)'}

+

Package: 5 Days / 4 Nights All-Inclusive

+

Destinations: Cancun, Cabo, Riviera Maya, Puerto Vallarta

+

Valid for: 18 months from today

+
+ +
+

100% Money-Back Guarantee

+

Full refund within 30 days, no questions asked.

+
+ +

What's Next?

+
    +
  1. Set up your account password below
  2. +
  3. Log in to your dashboard to view your certificate
  4. +
  5. Call 888-602-2424 to book your travel dates!
  6. +
+ + ${btn('Set Up My Account', `${APP_URL}/dashboard/login`)} + +
+

Ready to book? Call us now!

+

888-602-2424

+
+ `), + }) +} + +// ─── Payment Receipt ────────────────────────────────────── + +export async function sendPaymentReceiptEmail(email: string, name: string, amount: number, transactionId: string, paymentNumber: number, totalPayments: number) { + return sendMail({ + to: email, + subject: `Payment Received — $${amount.toFixed(2)} — Mexico Paradise Vacations`, + html: wrap(` +

Payment Received

+

Hi ${name}, your payment has been processed successfully.

+ +
+
+

Payment Receipt

+
+
+ + + + + +
Amount$${amount.toFixed(2)}
Payment${paymentNumber} of ${totalPayments}
Transaction ID${transactionId}
Date${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
+
+
+ + ${btn('View Billing History', `${APP_URL}/dashboard/billing`)} + `), + }) +} + +// ─── Password Reset ─────────────────────────────────────── + +export async function sendPasswordResetEmail(email: string, token: string) { + const resetUrl = `${APP_URL}/dashboard/reset-password?token=${token}` + + return sendMail({ + to: email, + subject: 'Reset Your Password — Mexico Paradise Vacations', + html: wrap(` +

Reset Your Password

+

We received a request to reset your password. Click the button below to create a new one.

+ + ${btn('Reset My Password', resetUrl)} + +

This link expires in 1 hour. If you didn't request this, you can safely ignore this email.

+ +
+

If the button doesn't work, copy this link:

+

${resetUrl}

+
+ `), + }) +} + +// ─── Certificate Email ──────────────────────────────────── + +export async function sendCertificateEmail(email: string, name: string, certificateNumber: string, expiryDate: string) { + return sendMail({ + to: email, + subject: `Your Vacation Certificate #${certificateNumber} — Mexico Paradise Vacations`, + html: wrap(` +

Your Vacation Certificate

+

Hi ${name}, here's your vacation certificate. Save this email for your records.

+ +
+

Vacation Certificate

+

MEXICO PARADISE VACATIONS

+

Presented To

+

${name}

+ +
+
+

Certificate No.

+

${certificateNumber}

+
+
+ +

Valid until: ${expiryDate}

+ +
+

5 Days / 4 Nights • All-Inclusive • 2 Guests

+

Cancun • Cabo San Lucas • Riviera Maya • Puerto Vallarta

+
+
+ +
+

To Book Your Vacation, Call

+

888-602-2424

+

Have your certificate number ready: ${certificateNumber}

+
+ + ${btn('View in Dashboard', `${APP_URL}/dashboard/certificate`)} + `), + }) +} + +// ─── Ebook Download Confirmation ────────────────────────── + +export async function sendEbookEmail(email: string, name?: string) { + return sendMail({ + to: email, + subject: 'Your Free Guide: Budget Luxury Travel in Mexico', + html: wrap(` +

Your Free Guide is Ready!

+

Hi${name ? ` ${name}` : ''}, thanks for downloading our Budget Luxury Travel guide. If your download didn't start, use the link below.

+ + ${btn('Download Guide (PDF)', `${APP_URL}/ebooks/budget-luxury-travel.pdf`, '#16A34A')} + +
+

Special Offer: $29/month

+

5 Days / 4 Nights All-Inclusive Mexico Vacation

+

100% money-back guarantee. Book immediately after first payment.

+ ${btn('Claim Your Vacation Certificate', APP_URL)} +
+ `), + }) +} + +// ─── Internal Admin Notification ────────────────────────── + +const ADMIN_NOTIFY = process.env.ADMIN_NOTIFY_EMAIL || 'nick@724care.net' + +/** Notify the team of a new lead / signup. Fire-and-forget; never blocks the user. */ +export async function sendAdminNotify(subject: string, fields: Record) { + const rows = Object.entries(fields) + .filter(([, v]) => v != null && String(v).trim() !== '') + .map(([k, v]) => `${k}${v}`) + .join('') + return sendMail({ + to: ADMIN_NOTIFY, + subject, + html: wrap(` +

${subject}

+ ${rows}
+ `), + }) +} + +// ─── Free Guide — Double Opt-in Verification ────────────── + +export async function sendVerifyEmail(email: string, name: string | undefined, verifyUrl: string) { + return sendMail({ + to: email, + subject: 'Confirm your email to get your free Mexico travel guide', + html: wrap(` +

One quick step, ${name || 'traveler'}!

+

Confirm your email and we'll unlock your free Budget Luxury Travel in Mexico guide right away.

+ + ${btn('Confirm & Get My Free Guide', verifyUrl, '#16A34A')} + +

If you didn't request this, you can ignore this email.

+ +
+

If the button doesn't work, copy this link:

+

${verifyUrl}

+
+ +
+

Ready now? Start from just $29/month

+

5 days / 4 nights all-inclusive Mexico. 100% money-back guarantee.

+ ${btn('See the $29/mo Offer', APP_URL)} +
+ `), + }) +} + +// ─── Drip Follow-ups ────────────────────────────────────── + +const PHONE_TEL = 'tel:+18886022424' + +function callBlock() { + return `
+

Questions? Talk to a real person:

+

(888) 602-2424

+
` +} + +// Leads who entered an email but never confirmed / never bought. +const LEAD_STEPS = [ + { + subject: 'Your Mexico getaway is still available', + body: () => ` +

Ready when you are

+

5 days / 4 nights all-inclusive in Mexico from just $29/mo, kids free. Pick up right where you left off.

+ ${btn('See My Deal', `${APP_URL}/#checkout`)}`, + }, + { + subject: '$29/mo, kids free — still thinking about Mexico?', + body: () => ` +

No pressure, just the facts

+

One honest ~60-minute resort presentation, a polite "no" is always fine, and a 100% refund within 30 days. Travel anytime in the next 18 months.

+ ${btn('Get My Certificate', `${APP_URL}/#checkout`)} + ${callBlock()}`, + }, + { + subject: 'Last call on your Mexico getaway', + body: () => ` +

We'll stop here

+

Last reminder about your 5-day all-inclusive Mexico deal. If it's still on your list, lock it in below or give us a call.

+ ${btn('Claim My Deal', `${APP_URL}/#checkout`)} + ${callBlock()}`, + }, +] + +export async function sendLeadFollowup(email: string, step: number) { + const s = LEAD_STEPS[step] + if (!s) return null + return sendMail({ to: email, subject: s.subject, html: wrap(s.body()) }) +} + +// People who started checkout but did not complete payment. +const ABANDONED_STEPS = [ + { + subject: "You're one step from paradise", + body: () => ` +

Almost there!

+

You started booking your 5-day all-inclusive Mexico getaway but didn't finish. Your spot is still here, from just $29/mo, with a 100% money-back guarantee.

+ ${btn('Finish My Booking', `${APP_URL}/#checkout`)} + ${callBlock()}`, + }, + { + subject: 'Still thinking about Mexico?', + body: () => ` +

No pressure, just honest info

+

The deal is simple: a real all-inclusive vacation, one ~60-minute resort presentation, and a polite "no" is always fine. 30-day full refund if you change your mind. Prefer to book by phone? We're happy to help.

+ ${btn('Complete My Booking', `${APP_URL}/#checkout`)} + ${callBlock()}`, + }, +] + +export async function sendAbandonedFollowup(email: string, step: number) { + const s = ABANDONED_STEPS[step] + if (!s) return null + return sendMail({ to: email, subject: s.subject, html: wrap(s.body()) }) +} + +// Paid customers who may not have booked their travel dates yet. +export async function sendBookingReminder(email: string, name: string) { + return sendMail({ + to: email, + subject: 'Have you booked your Mexico trip yet?', + html: wrap(` +

Ready when you are, ${name || 'traveler'}!

+

Your vacation certificate is active. The last step is picking your dates, our team books it for you over the phone.

+ ${callBlock()} +

Have your certificate number handy when you call.

`), + }) +} + +// ─── Affiliate Welcome ──────────────────────────────────── + +export async function sendAffiliateWelcomeEmail(email: string, name: string, referralCode: string) { + return sendMail({ + to: email, + subject: 'Welcome to the Affiliate Program — Mexico Paradise Vacations', + html: wrap(` +

Welcome, ${name}!

+

You're now part of the Mexico Paradise Vacations affiliate program. Start sharing your unique referral link and earn commission on every sale.

+ +
+

Your Referral Code

+

${referralCode}

+
+ +
+

Your Referral Link:

+

${APP_URL}/lp/golden-hour?ref=${referralCode}

+
+ +

How It Works

+
    +
  1. Share your referral link on social media, email, or your website
  2. +
  3. When someone purchases through your link, you earn 20% commission
  4. +
  5. Track your earnings in your affiliate dashboard
  6. +
  7. Get paid monthly via PayPal, bank transfer, or check
  8. +
+ + ${btn('Go to Affiliate Dashboard', `${APP_URL}/affiliate`, '#0D9488')} + `), + }) +} diff --git a/src/lib/maverick.ts b/src/lib/maverick.ts index 190dfe8..e61d071 100644 --- a/src/lib/maverick.ts +++ b/src/lib/maverick.ts @@ -1,152 +1,376 @@ -export interface MaverickPaymentRequest { - amount: number - currency: string +/** + * Maverick Payments API Integration + * + * Flow for vacation certificate purchase: + * 1. Create customer in Customer Vault + * 2. Add card to customer vault (tokenized) + * 3. Process first $39 sale + * 4. Set up recurring payment ($39/mo × 9 remaining months) + * + * API Docs: https://developers.maverickpayments.com + * Dashboard: https://dashboard.maverickpayments.com + */ + +const GATEWAY_URL = process.env.MAVERICK_GATEWAY_URL || 'https://gateway.maverickpayments.com' +const DASHBOARD_URL = process.env.MAVERICK_DASHBOARD_URL || 'https://dashboard.maverickpayments.com' +const API_TOKEN = process.env.MAVERICK_API_TOKEN || '' +const TERMINAL_ID = parseInt(process.env.MAVERICK_TERMINAL_ID || '0', 10) +const DBA_ID = parseInt(process.env.MAVERICK_DBA_ID || '0', 10) +const BILLING_ID = parseInt(process.env.MAVERICK_BILLING_ID || '0', 10) + +function headers() { + return { + 'Authorization': `Bearer ${API_TOKEN}`, + 'Content-Type': 'application/json', + } +} + +// ─── Customer Vault ──────────────────────────────────────── + +export interface CreateCustomerRequest { + firstName: string + lastName: string email: string - fullName: string + phone: string +} + +export interface CustomerResponse { + id: number + token: string + firstName: string + lastName: string + email: string +} + +export async function createCustomer(req: CreateCustomerRequest): Promise { + const res = await fetch(`${DASHBOARD_URL}/api/customer-vault`, { + method: 'POST', + headers: headers(), + body: JSON.stringify({ + dba: { id: DBA_ID }, + firstName: req.firstName, + lastName: req.lastName, + email: req.email, + phone: req.phone, + description: 'Mexico Paradise Vacations - Certificate Purchase', + }), + }) + + if (!res.ok) { + const err = await res.text() + throw new Error(`Failed to create customer: ${err}`) + } + + return res.json() +} + +// ─── Add Card to Vault ───────────────────────────────────── + +export interface AddCardRequest { + customerId: number + cardNumber: string + exp: string // MM/YY format + cvv: string + holderName: string +} + +export interface CardResponse { + id: number + number: number // last 4 digits + token: string + exp: string + status: string +} + +export async function addCardToVault(req: AddCardRequest): Promise { + const res = await fetch(`${DASHBOARD_URL}/api/customer-vault/${req.customerId}/card`, { + method: 'POST', + headers: headers(), + body: JSON.stringify({ + billing: { id: BILLING_ID }, + terminal: { id: TERMINAL_ID }, + holderName: req.holderName, + number: req.cardNumber, + exp: req.exp, + cvv: req.cvv, + }), + }) + + if (!res.ok) { + const err = await res.text() + throw new Error(`Failed to add card: ${err}`) + } + + return res.json() +} + +// ─── Process Sale ────────────────────────────────────────── + +export interface SaleRequest { + amount: number // in dollars (e.g., 39.00) + cardToken: string description?: string - returnUrl?: string - cancelUrl?: string } -export interface MaverickPaymentResponse { - success: boolean - paymentId?: string - checkoutUrl?: string - error?: string +export interface SaleResponse { + id: string + status: string + amount: string + card: { + token: string + number: number + } } -export interface MaverickRefundRequest { - paymentId: string - amount?: number - reason?: string +export async function processSale(req: SaleRequest): Promise { + const res = await fetch(`${GATEWAY_URL}/payment/sale`, { + method: 'POST', + headers: headers(), + body: JSON.stringify({ + terminal: { id: TERMINAL_ID }, + amount: req.amount.toFixed(2), + source: 'Internet', + level: 1, + card: { + token: req.cardToken, + }, + }), + }) + + if (!res.ok) { + const err = await res.text() + throw new Error(`Sale failed: ${err}`) + } + + return res.json() } -export interface MaverickRefundResponse { - success: boolean - refundId?: string - error?: string +// ─── Tokenize Card (without charging) ────────────────────── + +export interface TokenizeRequest { + cardName: string + cardNumber: string + exp: string // MM/YY + cvv: string +} + +export interface TokenizeResponse { + token: string + card: { + number: number + brand: string + } } -export class MaverickPaymentAPI { - private baseUrl: string - private apiKey: string +export async function tokenizeCard(req: TokenizeRequest): Promise { + const res = await fetch(`${GATEWAY_URL}/payment/generate-token`, { + method: 'POST', + headers: headers(), + body: JSON.stringify({ + terminal: { id: TERMINAL_ID }, + source: 'Internet', + card: { + name: req.cardName, + number: req.cardNumber, + exp: req.exp, + cvv: req.cvv, + }, + }), + }) - constructor(baseUrl: string, apiKey: string) { - this.baseUrl = baseUrl - this.apiKey = apiKey + if (!res.ok) { + const err = await res.text() + throw new Error(`Tokenization failed: ${err}`) } - async createPayment(request: MaverickPaymentRequest): Promise { - try { - const response = await fetch(`${this.baseUrl}/payments`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.apiKey}`, - }, - body: JSON.stringify({ - amount: request.amount, - currency: request.currency, - customer: { - email: request.email, - name: request.fullName, - }, - description: request.description || 'Service Payment', - return_url: request.returnUrl, - cancel_url: request.cancelUrl, - }), - }) + return res.json() +} - const data = await response.json() - - if (response.ok) { - return { - success: true, - paymentId: data.id, - checkoutUrl: data.checkout_url, - } - } else { - return { - success: false, - error: data.error || 'Payment creation failed', - } - } - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Network error', - } - } +// ─── Create Recurring Payment ────────────────────────────── + +export interface RecurringPaymentRequest { + customerId: number + cardId: number + amount: number + name: string + description: string + maxPayments: number // e.g., 9 (remaining after first charge) + startDate: string // YYYY-MM-DD (next month) +} + +export interface RecurringPaymentResponse { + id: string + name: string + amount: string + execute: { + frequency: number + period: string + } + payment: { + next: string + max: number } + status: string | null +} - async getPaymentStatus(paymentId: string): Promise<{ success: boolean; status?: string; error?: string }> { - try { - const response = await fetch(`${this.baseUrl}/payments/${paymentId}`, { - headers: { - 'Authorization': `Bearer ${this.apiKey}`, - }, - }) +export async function createRecurringPayment(req: RecurringPaymentRequest): Promise { + // Calculate end date (maxPayments months from start) + const start = new Date(req.startDate) + const end = new Date(start) + end.setMonth(end.getMonth() + req.maxPayments) - const data = await response.json() - - if (response.ok) { - return { - success: true, - status: data.status, - } - } else { - return { - success: false, - error: data.error || 'Failed to get payment status', - } - } - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Network error', - } - } + const res = await fetch(`${DASHBOARD_URL}/api/customer-vault/${req.customerId}/recurring-payment`, { + method: 'POST', + headers: headers(), + body: JSON.stringify({ + name: req.name, + description: req.description, + amount: req.amount, + execute: { + frequency: 1, + period: 'month', + }, + valid: { + from: req.startDate, + to: end.toISOString().split('T')[0], + }, + payment: { + max: req.maxPayments, + }, + terminal: { id: TERMINAL_ID }, + customer: { + id: req.customerId, + card: { id: req.cardId }, + }, + dba: { id: DBA_ID }, + }), + }) + + if (!res.ok) { + const err = await res.text() + throw new Error(`Recurring payment setup failed: ${err}`) } - async createRefund(request: MaverickRefundRequest): Promise { - try { - const response = await fetch(`${this.baseUrl}/refunds`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.apiKey}`, - }, - body: JSON.stringify({ - payment_id: request.paymentId, - amount: request.amount, - reason: request.reason || 'Customer refund', - }), + return res.json() +} + +// ─── Refund ──────────────────────────────────────────────── + +export interface RefundRequest { + transactionId: string + amount?: number +} + +export interface RefundResponse { + id: string + status: string + amount: string +} + +export async function processRefund(req: RefundRequest): Promise { + const res = await fetch(`${GATEWAY_URL}/payment/refund`, { + method: 'POST', + headers: headers(), + body: JSON.stringify({ + terminal: { id: TERMINAL_ID }, + transactionId: req.transactionId, + ...(req.amount && { amount: req.amount.toFixed(2) }), + }), + }) + + if (!res.ok) { + const err = await res.text() + throw new Error(`Refund failed: ${err}`) + } + + return res.json() +} + +// ─── Full Purchase Flow ──────────────────────────────────── +// This is the main function used by the payment API route + +export interface PurchaseRequest { + firstName: string + lastName: string + email: string + phone: string + cardNumber: string + cardExp: string // MM/YY + cardCvv: string + cardName: string + paymentType: 'monthly' | 'one-time' +} + +export interface PurchaseResult { + success: boolean + customerId?: number + cardId?: number + transactionId?: string + recurringPaymentId?: string + error?: string +} + +export async function processFullPurchase(req: PurchaseRequest): Promise { + try { + // Step 1: Create customer in vault + const customer = await createCustomer({ + firstName: req.firstName, + lastName: req.lastName, + email: req.email, + phone: req.phone, + }) + + // Step 2: Add card to customer vault + const card = await addCardToVault({ + customerId: customer.id, + cardNumber: req.cardNumber, + exp: req.cardExp, + cvv: req.cardCvv, + holderName: req.cardName, + }) + + // Step 3: Process first payment + const amount = req.paymentType === 'monthly' ? 39.00 : 399.00 + const sale = await processSale({ + amount, + cardToken: card.token, + description: req.paymentType === 'monthly' + ? 'Mexico Paradise Vacation Certificate - Payment 1 of 10' + : 'Mexico Paradise Vacation Certificate - Full Payment', + }) + + // Step 4: If monthly, set up recurring for remaining 9 payments + let recurringPaymentId: string | undefined + if (req.paymentType === 'monthly') { + const nextMonth = new Date() + nextMonth.setMonth(nextMonth.getMonth() + 1) + const startDate = nextMonth.toISOString().split('T')[0] + + const recurring = await createRecurringPayment({ + customerId: customer.id, + cardId: card.id, + amount: 39.00, + name: 'Mexico Paradise Vacation - Monthly Payment', + description: 'Vacation certificate payment plan - $39/month', + maxPayments: 9, + startDate, }) - const data = await response.json() - - if (response.ok) { - return { - success: true, - refundId: data.id, - } - } else { - return { - success: false, - error: data.error || 'Refund creation failed', - } - } - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Network error', - } + recurringPaymentId = recurring.id + } + + return { + success: true, + customerId: customer.id, + cardId: card.id, + transactionId: sale.id, + recurringPaymentId, + } + } catch (error) { + console.error('Purchase flow error:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'Payment processing failed', } } } - -// Initialize with environment variables or defaults -export const maverickAPI = new MaverickPaymentAPI( - process.env.MAVERICK_API_URL || 'https://api.maverickpayments.com/v1', - process.env.MAVERICK_API_KEY || 'your-api-key-here' -) \ No newline at end of file diff --git a/src/lib/nmi.ts b/src/lib/nmi.ts new file mode 100644 index 0000000..cf5885f --- /dev/null +++ b/src/lib/nmi.ts @@ -0,0 +1,292 @@ +/** + * NMI Payment Gateway Integration + * API: https://secure.nmi.com/api/transact.php + * Docs: https://docs.nmi.com + */ + +const NMI_API_URL = process.env.NMI_API_URL || 'https://secure.nmi.com/api/transact.php' +const SECURITY_KEY = process.env.NMI_SECURITY_KEY || '' + +interface NMIResponse { + response: string // '1' = approved, '2' = declined, '3' = error + responsetext: string + authcode?: string + transactionid?: string + customer_vault_id?: string + subscription_id?: string + response_code?: string +} + +async function nmiRequest(params: Record): Promise { + params.security_key = SECURITY_KEY + + const body = new URLSearchParams(params).toString() + const res = await fetch(NMI_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body, + }) + + const text = await res.text() + const parsed: Record = {} + text.split('&').forEach(pair => { + const [key, ...vals] = pair.split('=') + parsed[decodeURIComponent(key)] = decodeURIComponent(vals.join('=')) + }) + + return parsed as unknown as NMIResponse +} + +// ─── Process Sale ────────────────────────────────────────── + +export async function processSale(params: { + amount: number + firstName: string + lastName: string + email: string + phone?: string + zip?: string + orderDescription?: string + // Either token OR card details + paymentToken?: string + cardNumber?: string + cardExp?: string + cardCvv?: string +}): Promise { + const data: Record = { + type: 'sale', + amount: params.amount.toFixed(2), + first_name: params.firstName, + last_name: params.lastName, + email: params.email, + phone: params.phone || '', + order_description: params.orderDescription || 'Mexico Paradise Vacation Certificate', + orderid: `HI2B-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + } + if (params.zip) data.zip = params.zip // AVS + + if (params.paymentToken) { + data.payment_token = params.paymentToken + } else if (params.cardNumber) { + data.ccnumber = params.cardNumber + data.ccexp = (params.cardExp || '').replace('/', '') + data.cvv = params.cardCvv || '' + } + + return nmiRequest(data) +} + +// ─── Add Customer to Vault ───────────────────────────────── + +export async function addToVault(params: { + firstName: string + lastName: string + email: string + phone?: string + zip?: string + paymentToken?: string + cardNumber?: string + cardExp?: string + cardCvv?: string +}): Promise { + const data: Record = { + customer_vault: 'add_customer', + first_name: params.firstName, + last_name: params.lastName, + email: params.email, + phone: params.phone || '', + } + if (params.zip) data.zip = params.zip // AVS + + if (params.paymentToken) { + data.payment_token = params.paymentToken + } else if (params.cardNumber) { + data.ccnumber = params.cardNumber + data.ccexp = (params.cardExp || '').replace('/', '') + data.cvv = params.cardCvv || '' + } + + return nmiRequest(data) +} + +// ─── Create Recurring Subscription ───────────────────────── +// Accepts EITHER a customer_vault_id OR card details directly. Live merchant +// accounts without Customer Vault enabled can still bill recurring by passing +// ccnumber/ccexp directly to add_subscription. + +export async function createSubscription(params: { + customerVaultId?: string + paymentToken?: string + cardNumber?: string + cardExp?: string + cardCvv?: string + planPayments: number + planAmount: number + monthFrequency?: number + dayOfMonth?: number + startDate?: string + firstName: string + lastName: string + email: string + phone?: string +}): Promise { + const now = new Date() + const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, now.getDate()) + const defaultStartDate = nextMonth.toISOString().slice(0, 10).replace(/-/g, '') + + const data: Record = { + recurring: 'add_subscription', + plan_payments: params.planPayments.toString(), + plan_amount: params.planAmount.toFixed(2), + month_frequency: (params.monthFrequency || 1).toString(), + day_of_month: (params.dayOfMonth || now.getDate()).toString(), + start_date: params.startDate || defaultStartDate, + first_name: params.firstName, + last_name: params.lastName, + email: params.email, + phone: params.phone || '', + } + + if (params.customerVaultId) { + data.customer_vault_id = params.customerVaultId + } else if (params.paymentToken) { + data.payment_token = params.paymentToken + } else if (params.cardNumber) { + data.ccnumber = params.cardNumber + data.ccexp = (params.cardExp || '').replace('/', '') + if (params.cardCvv) data.cvv = params.cardCvv + } + + return nmiRequest(data) +} + +// ─── Refund ──────────────────────────────────────────────── + +export async function processRefund(params: { + transactionId: string + amount?: number +}): Promise { + const data: Record = { + type: 'refund', + transactionid: params.transactionId, + } + if (params.amount) data.amount = params.amount.toFixed(2) + return nmiRequest(data) +} + +// ─── Full Purchase Flow ──────────────────────────────────── + +export interface PurchaseRequest { + firstName: string + lastName: string + email: string + phone: string + zip?: string + paymentType: 'monthly' | 'one-time' + paymentToken?: string + cardNumber?: string + cardExp?: string + cardCvv?: string +} + +export interface PurchaseResult { + success: boolean + transactionId?: string + customerVaultId?: string + subscriptionId?: string + error?: string +} + +export async function processFullPurchase(req: PurchaseRequest): Promise { + try { + const amount = req.paymentType === 'monthly' ? 29.00 : 249.00 + + // Step 1: Process initial sale + const sale = await processSale({ + amount, + firstName: req.firstName, + lastName: req.lastName, + email: req.email, + phone: req.phone, + zip: req.zip, + paymentToken: req.paymentToken, + cardNumber: req.cardNumber, + cardExp: req.cardExp, + cardCvv: req.cardCvv, + orderDescription: req.paymentType === 'monthly' + ? 'Mexico Paradise Vacation Certificate - Payment 1 of 10' + : 'Mexico Paradise Vacation Certificate - Full Payment', + }) + + if (sale.response !== '1') { + return { + success: false, + error: sale.responsetext || 'Payment declined', + } + } + + let customerVaultId: string | undefined + let subscriptionId: string | undefined + + // Step 2: If monthly, create the recurring subscription for the remaining + // 9 payments. Try Customer Vault first (cleanest record-keeping); if the + // merchant account doesn't have Vault enabled, fall back to passing card + // details straight to add_subscription — NMI accepts both flows. + if (req.paymentType === 'monthly') { + const vault = await addToVault({ + firstName: req.firstName, + lastName: req.lastName, + email: req.email, + phone: req.phone, + zip: req.zip, + paymentToken: req.paymentToken, + cardNumber: req.cardNumber, + cardExp: req.cardExp, + cardCvv: req.cardCvv, + }) + + if (vault.response === '1') { + customerVaultId = vault.customer_vault_id + } else { + console.warn('Vault unavailable, creating subscription with card data directly:', vault.responsetext) + } + + const subscription = await createSubscription({ + ...(customerVaultId + ? { customerVaultId } + : { + paymentToken: req.paymentToken, + cardNumber: req.cardNumber, + cardExp: req.cardExp, + cardCvv: req.cardCvv, + }), + planPayments: 9, + planAmount: 29.00, + monthFrequency: 1, + firstName: req.firstName, + lastName: req.lastName, + email: req.email, + phone: req.phone, + }) + + if (subscription.response === '1') { + subscriptionId = subscription.subscription_id + } else { + console.error('Subscription failed:', subscription.responsetext) + } + } + + return { + success: true, + transactionId: sale.transactionid, + customerVaultId, + subscriptionId, + } + } catch (error) { + console.error('Purchase error:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'Payment processing failed', + } + } +} diff --git a/src/lib/openrouter.ts b/src/lib/openrouter.ts new file mode 100644 index 0000000..ff80756 --- /dev/null +++ b/src/lib/openrouter.ts @@ -0,0 +1,57 @@ +// OpenRouter LLM client — routes to the best available FREE model, with an +// automatic fallback because free models get rate-limited upstream. +// Key lives in .env (OPENROUTER_API_KEY); never hard-code it. + +const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions' +const KEY = process.env.OPENROUTER_API_KEY + +// Strongest working free general model first; openrouter/free auto-routes as fallback. +const DEFAULT_MODELS = [ + 'nvidia/nemotron-3-ultra-550b-a55b:free', + 'openrouter/free', +] + +export interface LLMMessage { + role: 'system' | 'user' | 'assistant' + content: string +} + +export interface LLMOptions { + model?: string // override the default free model + maxTokens?: number + temperature?: number +} + +/** Call OpenRouter and return the assistant's text. Throws on total failure. */ +export async function llm(messages: LLMMessage[], opts: LLMOptions = {}): Promise { + if (!KEY) throw new Error('OPENROUTER_API_KEY not set in .env') + const models = opts.model ? [opts.model] : DEFAULT_MODELS + + let lastErr = 'unknown error' + for (const model of models) { + try { + const res = await fetch(OPENROUTER_URL, { + method: 'POST', + headers: { + Authorization: `Bearer ${KEY}`, + 'Content-Type': 'application/json', + 'HTTP-Referer': 'https://hi2b.com', + 'X-Title': 'hi2b', + }, + body: JSON.stringify({ + model, + messages, + max_tokens: opts.maxTokens ?? 1024, + temperature: opts.temperature ?? 0.7, + }), + }) + const data = await res.json().catch(() => ({})) + const content: string | undefined = data?.choices?.[0]?.message?.content + if (res.ok && content) return content + lastErr = data?.error?.message || `HTTP ${res.status}` + } catch (e) { + lastErr = e instanceof Error ? e.message : 'network error' + } + } + throw new Error(`OpenRouter failed (all models): ${lastErr}`) +} diff --git a/src/lib/verify-token.ts b/src/lib/verify-token.ts new file mode 100644 index 0000000..382c006 --- /dev/null +++ b/src/lib/verify-token.ts @@ -0,0 +1,17 @@ +import crypto from 'crypto' + +// Stateless email-verification token (double opt-in). No DB column needed: +// the token is an HMAC of the lowercased email, so /api/verify can validate it +// without storing anything. Low-stakes (free-guide opt-in), so no expiry. + +const SECRET = process.env.VERIFY_SECRET || process.env.MAIL_API_KEY || 'hi2b-dev-secret' + +export function makeVerifyToken(email: string): string { + return crypto.createHmac('sha256', SECRET).update(email.trim().toLowerCase()).digest('hex').slice(0, 32) +} + +export function checkVerifyToken(email: string, token: string): boolean { + const expected = makeVerifyToken(email) + if (token.length !== expected.length) return false + return crypto.timingSafeEqual(Buffer.from(token), Buffer.from(expected)) +}