Add Venmo payment step with QR code and configurable username (issue #7)
Deploy to Dev / Deploy & Smoke Test (push) Successful in 27s
Deploy to Dev / Deploy & Smoke Test (push) Successful in 27s
Step reorder (still 7 steps): Info (5) → Contract (6) → Payment (7) - Client info moved to step 5 so it's available for the contract signature name and the personalised Venmo note before the payment step - Contract is now step 6 (initContractStep triggered on step 6 entry) - Step 7 is the full payment step — no separate confirm step needed Payment step (step 7): - Compact summary table: date, session type, length, location - Large price display from siteConfig.booking.pricing - "Pay on Venmo" button linking to: https://venmo.com/<username>?txn=pay&amount=<price>¬e=<encoded-note> Note format: "LisiLou Booking - [SessionType] on [Date]" - QR code rendered from GET /api/venmo-qr?url=<encoded> (server-side SVG via qrcode package, site colours applied) - "I've sent payment" checkbox gates the Submit button - Clicking Submit creates booking with payment_status='pending_confirmation', then signs the contract — success screen shown immediately Configuration: venmoUsername lives in config/site.json under booking.venmoUsername — change it there to point at any Venmo account with no code change and no rebuild. API additions: - GET /api/venmo-qr?url= — validates url starts with venmo.com, returns SVG - POST /api/bookings now accepts payment_status field Closes #7 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Generated
+307
-1
@@ -12,7 +12,8 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.18.3",
|
"express": "^4.18.3",
|
||||||
"pdf-lib": "^1.17.1"
|
"pdf-lib": "^1.17.1",
|
||||||
|
"qrcode": "^1.5.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@pdf-lib/standard-fonts": {
|
"node_modules/@pdf-lib/standard-fonts": {
|
||||||
@@ -46,6 +47,30 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/array-flatten": {
|
"node_modules/array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
@@ -189,12 +214,50 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/camelcase": {
|
||||||
|
"version": "5.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
|
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chownr": {
|
"node_modules/chownr": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.0",
|
||||||
|
"wrap-ansi": "^6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
@@ -257,6 +320,15 @@
|
|||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decamelize": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/decompress-response": {
|
"node_modules/decompress-response": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
@@ -309,6 +381,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dijkstrajs": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.6.1",
|
"version": "16.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
@@ -341,6 +419,12 @@
|
|||||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
@@ -483,6 +567,19 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/find-up": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"locate-path": "^5.0.0",
|
||||||
|
"path-exists": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -516,6 +613,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
@@ -668,6 +774,27 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/locate-path": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-locate": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -839,6 +966,42 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/p-limit": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-try": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-locate": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-limit": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-try": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pako": {
|
"node_modules/pako": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
@@ -854,6 +1017,15 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-exists": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||||
@@ -872,6 +1044,15 @@
|
|||||||
"tslib": "^1.11.1"
|
"tslib": "^1.11.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pngjs": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prebuild-install": {
|
"node_modules/prebuild-install": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||||
@@ -922,6 +1103,23 @@
|
|||||||
"once": "^1.3.1"
|
"once": "^1.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dijkstrajs": "^1.0.1",
|
||||||
|
"pngjs": "^5.0.0",
|
||||||
|
"yargs": "^15.3.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"qrcode": "bin/qrcode"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.15.3",
|
"version": "6.15.3",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
|
||||||
@@ -991,6 +1189,21 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/require-main-filename": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
@@ -1074,6 +1287,12 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-blocking": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/setprototypeof": {
|
"node_modules/setprototypeof": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
@@ -1215,6 +1434,32 @@
|
|||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/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-json-comments": {
|
"node_modules/strip-json-comments": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
@@ -1325,11 +1570,72 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/which-module": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wrappy": {
|
"node_modules/wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "15.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^6.0.0",
|
||||||
|
"decamelize": "^1.2.0",
|
||||||
|
"find-up": "^4.1.0",
|
||||||
|
"get-caller-file": "^2.0.1",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"require-main-filename": "^2.0.0",
|
||||||
|
"set-blocking": "^2.0.0",
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"which-module": "^2.0.0",
|
||||||
|
"y18n": "^4.0.0",
|
||||||
|
"yargs-parser": "^18.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "18.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||||
|
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"camelcase": "^5.0.0",
|
||||||
|
"decamelize": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -11,6 +11,7 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.18.3",
|
"express": "^4.18.3",
|
||||||
"pdf-lib": "^1.17.1"
|
"pdf-lib": "^1.17.1",
|
||||||
|
"qrcode": "^1.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-3
@@ -33,6 +33,7 @@ app.post('/api/bookings', (req, res) => {
|
|||||||
const {
|
const {
|
||||||
client_name, client_email, client_phone, client_sub,
|
client_name, client_email, client_phone, client_sub,
|
||||||
session_date, session_type, session_length, location,
|
session_date, session_type, session_length, location,
|
||||||
|
payment_status,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!client_email || !session_date || !session_type || !session_length) {
|
if (!client_email || !session_date || !session_type || !session_length) {
|
||||||
@@ -41,13 +42,37 @@ app.post('/api/bookings', (req, res) => {
|
|||||||
|
|
||||||
const result = db.prepare(`
|
const result = db.prepare(`
|
||||||
INSERT INTO bookings
|
INSERT INTO bookings
|
||||||
(client_name, client_email, client_phone, client_sub, session_date, session_type, session_length, location)
|
(client_name, client_email, client_phone, client_sub, session_date, session_type, session_length, location, payment_status)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(client_name, client_email, client_phone, client_sub, session_date, session_type, session_length, location);
|
`).run(
|
||||||
|
client_name, client_email, client_phone, client_sub,
|
||||||
|
session_date, session_type, session_length, location,
|
||||||
|
payment_status || 'pending',
|
||||||
|
);
|
||||||
|
|
||||||
res.status(201).json({ id: result.lastInsertRowid });
|
res.status(201).json({ id: result.lastInsertRowid });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Generate a QR code SVG for any URL — used by the Venmo payment step.
|
||||||
|
// Venmo username and amount come from the frontend (which reads site.json),
|
||||||
|
// so changing venmoUsername in config updates both the button and QR with
|
||||||
|
// no server restart needed.
|
||||||
|
app.get('/api/venmo-qr', async (req, res) => {
|
||||||
|
const { url } = req.query;
|
||||||
|
if (!url || !url.startsWith('https://venmo.com/')) {
|
||||||
|
return res.status(400).json({ error: 'url must be a venmo.com URL' });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const QRCode = require('qrcode');
|
||||||
|
const svg = await QRCode.toString(url, { type: 'svg', margin: 2, width: 220, color: { dark: '#2C2C2C', light: '#FDFBF9' } });
|
||||||
|
res.setHeader('Content-Type', 'image/svg+xml');
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||||
|
res.send(svg);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'QR generation failed' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Stamp the signature onto the contract PDF and record it
|
// Stamp the signature onto the contract PDF and record it
|
||||||
app.post('/api/bookings/:id/sign', async (req, res) => {
|
app.post('/api/bookings/:id/sign', async (req, res) => {
|
||||||
const id = parseInt(req.params.id, 10);
|
const id = parseInt(req.params.id, 10);
|
||||||
|
|||||||
+189
-74
@@ -1115,15 +1115,67 @@
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
margin: 0.5rem 0 1.25rem;
|
margin: 0.5rem 0 1.25rem;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.payment-note {
|
.payment-note {
|
||||||
background: var(--color-bg-warm);
|
background: var(--color-bg-warm);
|
||||||
border-left: 2px solid var(--color-primary);
|
border-left: 2px solid var(--color-primary);
|
||||||
padding: 1rem 1.25rem;
|
padding: 0.9rem 1.25rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.25rem;
|
||||||
font-size: 0.85rem;
|
font-size: 0.8rem;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-summary {
|
||||||
|
border: 1px solid var(--color-accent);
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-summary-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.5rem 0.9rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
border-bottom: 1px solid var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-summary-row:last-child { border-bottom: none; }
|
||||||
|
.pay-summary-row span:first-child { color: var(--color-text-light); }
|
||||||
|
.pay-summary-row span:last-child { font-weight: 500; color: var(--color-text); text-align: right; max-width: 60%; }
|
||||||
|
|
||||||
|
.venmo-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.85rem 1.5rem;
|
||||||
|
background: #3D95CE;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venmo-btn:hover { background: #2d7db0; }
|
||||||
|
|
||||||
|
.venmo-qr {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venmo-qr svg {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
border: 1px solid var(--color-accent);
|
||||||
|
padding: 6px;
|
||||||
|
background: var(--color-bg-warm);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Confirm / summary */
|
/* Confirm / summary */
|
||||||
@@ -1353,9 +1405,9 @@
|
|||||||
<div class="booking-step-item" data-step="2"><div class="step-dot">2</div><span class="step-label-text">Session</span></div>
|
<div class="booking-step-item" data-step="2"><div class="step-dot">2</div><span class="step-label-text">Session</span></div>
|
||||||
<div class="booking-step-item" data-step="3"><div class="step-dot">3</div><span class="step-label-text">Length</span></div>
|
<div class="booking-step-item" data-step="3"><div class="step-dot">3</div><span class="step-label-text">Length</span></div>
|
||||||
<div class="booking-step-item" data-step="4"><div class="step-dot">4</div><span class="step-label-text">Location</span></div>
|
<div class="booking-step-item" data-step="4"><div class="step-dot">4</div><span class="step-label-text">Location</span></div>
|
||||||
<div class="booking-step-item" data-step="5"><div class="step-dot">5</div><span class="step-label-text">Contract</span></div>
|
<div class="booking-step-item" data-step="5"><div class="step-dot">5</div><span class="step-label-text">Info</span></div>
|
||||||
<div class="booking-step-item" data-step="6"><div class="step-dot">6</div><span class="step-label-text">Payment</span></div>
|
<div class="booking-step-item" data-step="6"><div class="step-dot">6</div><span class="step-label-text">Contract</span></div>
|
||||||
<div class="booking-step-item" data-step="7"><div class="step-dot">7</div><span class="step-label-text">Confirm</span></div>
|
<div class="booking-step-item" data-step="7"><div class="step-dot">7</div><span class="step-label-text">Payment</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1406,17 +1458,35 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 5: Contract -->
|
<!-- Step 5: Contract -->
|
||||||
|
<!-- Step 5: Your Info -->
|
||||||
<div class="step-panel" id="step-5">
|
<div class="step-panel" id="step-5">
|
||||||
|
<h3 class="step-heading">Your Information</h3>
|
||||||
|
<p class="step-subheading">How should we reach you to confirm your session?</p>
|
||||||
|
<div class="booking-field">
|
||||||
|
<label class="booking-label" for="client-name">Full Name</label>
|
||||||
|
<input type="text" class="booking-input" id="client-name" placeholder="Your full name" autocomplete="name">
|
||||||
|
</div>
|
||||||
|
<div class="booking-field">
|
||||||
|
<label class="booking-label" for="client-email">Email Address</label>
|
||||||
|
<input type="email" class="booking-input" id="client-email" placeholder="your@email.com" autocomplete="email">
|
||||||
|
</div>
|
||||||
|
<div class="booking-field">
|
||||||
|
<label class="booking-label" for="client-phone">Phone Number</label>
|
||||||
|
<input type="tel" class="booking-input" id="client-phone" placeholder="(555) 000-0000" autocomplete="tel">
|
||||||
|
</div>
|
||||||
|
<div class="booking-error" id="info-error" style="display:none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 6: Contract — filled by initContractStep() -->
|
||||||
|
<div class="step-panel" id="step-6">
|
||||||
<h3 class="step-heading">Model Release & Contract</h3>
|
<h3 class="step-heading">Model Release & Contract</h3>
|
||||||
<p class="step-subheading">Read the full agreement below, then sign and type your name.</p>
|
<p class="step-subheading">Read the full agreement below, then sign and type your name.</p>
|
||||||
|
|
||||||
<!-- PDF viewer — filled by initContractStep() -->
|
|
||||||
<div class="contract-viewer" id="contract-viewer">
|
<div class="contract-viewer" id="contract-viewer">
|
||||||
<div class="contract-loading" id="contract-loading">Loading contract…</div>
|
<div class="contract-loading" id="contract-loading">Loading contract…</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="contract-scroll-notice" id="contract-scroll-notice">Scroll to the bottom to enable signing ↓</div>
|
<div class="contract-scroll-notice" id="contract-scroll-notice">Scroll to the bottom to enable signing ↓</div>
|
||||||
|
|
||||||
<!-- Signature pad — locked until scrolled -->
|
|
||||||
<div class="sig-section locked" id="sig-section">
|
<div class="sig-section locked" id="sig-section">
|
||||||
<div class="booking-label" style="margin-bottom:0.4rem;">Your Signature</div>
|
<div class="booking-label" style="margin-bottom:0.4rem;">Your Signature</div>
|
||||||
<div class="sig-pad-wrapper">
|
<div class="sig-pad-wrapper">
|
||||||
@@ -1432,45 +1502,50 @@
|
|||||||
<div class="booking-error" id="contract-error" style="display:none;"></div>
|
<div class="booking-error" id="contract-error" style="display:none;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 6: Payment -->
|
<!-- Step 7: Payment — filled by initPaymentStep() -->
|
||||||
<div class="step-panel" id="step-6">
|
<div class="step-panel" id="step-7">
|
||||||
<h3 class="step-heading">Booking Fee</h3>
|
<h3 class="step-heading">Booking Fee</h3>
|
||||||
<p class="step-subheading">A non-refundable booking fee reserves your date.</p>
|
<p class="step-subheading">Send payment via Venmo to reserve your date.</p>
|
||||||
<div class="payment-amount" id="payment-amount">—</div>
|
|
||||||
<div class="payment-note">
|
<div class="pay-summary" id="pay-summary">
|
||||||
Send payment via Venmo to reserve your session. Your date is not confirmed until payment is received.
|
<div class="pay-summary-row"><span>Date</span><span id="pay-summary-date">—</span></div>
|
||||||
|
<div class="pay-summary-row"><span>Session</span><span id="pay-summary-type">—</span></div>
|
||||||
|
<div class="pay-summary-row"><span>Length</span><span id="pay-summary-length">—</span></div>
|
||||||
|
<div class="pay-summary-row"><span>Location</span><span id="pay-summary-location">—</span></div>
|
||||||
</div>
|
</div>
|
||||||
<p style="font-size:0.82rem;color:var(--color-text-light);margin-bottom:1.5rem;">Venmo link and QR code will appear here — Issue #7</p>
|
|
||||||
|
<div class="payment-amount" id="payment-amount">—</div>
|
||||||
|
|
||||||
|
<a class="venmo-btn" id="venmo-link" href="#" target="_blank" rel="noopener">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="currentColor" style="vertical-align:middle;margin-right:0.4rem"><path d="M15.25 1C13.5 4.25 11.1 8.15 9.5 11H6.75L5 2.25l-4 .75 2.25 12.5h5.5C11.5 11.5 16.5 4.25 17 1h-1.75z"/></svg>
|
||||||
|
Pay on Venmo
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="venmo-qr" id="venmo-qr"></div>
|
||||||
|
|
||||||
|
<p class="payment-note">
|
||||||
|
Your session is reserved once we confirm receipt — usually within 24 hours.
|
||||||
|
No Venmo? Send the exact amount and session note via any method and check the box below.
|
||||||
|
</p>
|
||||||
|
|
||||||
<label class="booking-checkbox-label">
|
<label class="booking-checkbox-label">
|
||||||
<input type="checkbox" id="payment-sent">
|
<input type="checkbox" id="payment-sent">
|
||||||
I have sent the booking fee payment via Venmo.
|
I've sent the booking fee
|
||||||
</label>
|
</label>
|
||||||
<div class="booking-error" id="payment-error" style="display:none;">Please confirm you've sent payment to continue.</div>
|
<div class="booking-error" id="payment-error" style="display:none;">Please confirm you've sent payment to continue.</div>
|
||||||
|
<div class="booking-error" id="confirm-error" style="display:none;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 7: Confirm -->
|
<!-- REMOVED step-7 confirm panel — client info now in step 5 -->
|
||||||
<div class="step-panel" id="step-7">
|
<!-- keep a dummy anchor so old references still resolve -->
|
||||||
<h3 class="step-heading">Almost There!</h3>
|
<div style="display:none">
|
||||||
<p class="step-subheading">Review your details and enter your contact information.</p>
|
<span id="summary-date"></span><span id="summary-type"></span>
|
||||||
<div class="confirm-summary">
|
<span id="summary-length"></span><span id="summary-location"></span>
|
||||||
<div class="confirm-row"><span class="confirm-label">Date</span><span class="confirm-value" id="summary-date">—</span></div>
|
<input id="REMOVED_client-name">
|
||||||
<div class="confirm-row"><span class="confirm-label">Session Type</span><span class="confirm-value" id="summary-type">—</span></div>
|
|
||||||
<div class="confirm-row"><span class="confirm-label">Length</span><span class="confirm-value" id="summary-length">—</span></div>
|
|
||||||
<div class="confirm-row"><span class="confirm-label">Location</span><span class="confirm-value" id="summary-location">—</span></div>
|
|
||||||
</div>
|
|
||||||
<div class="booking-field">
|
|
||||||
<label class="booking-label" for="client-name">Your Name</label>
|
|
||||||
<input type="text" class="booking-input" id="client-name" placeholder="Full name" autocomplete="name">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="booking-field">
|
<div class="booking-field">
|
||||||
<label class="booking-label" for="client-email">Email Address</label>
|
<label class="booking-label" for="client-email">Email Address</label>
|
||||||
<input type="email" class="booking-input" id="client-email" placeholder="your@email.com" autocomplete="email">
|
<input type="email" class="booking-input" id="client-email" placeholder="your@email.com" autocomplete="email">
|
||||||
</div>
|
|
||||||
<div class="booking-field">
|
|
||||||
<label class="booking-label" for="client-phone">Phone Number</label>
|
|
||||||
<input type="tel" class="booking-input" id="client-phone" placeholder="(555) 000-0000" autocomplete="tel">
|
|
||||||
</div>
|
|
||||||
<div class="booking-error" id="confirm-error" style="display:none;"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Success -->
|
<!-- Success -->
|
||||||
@@ -1993,6 +2068,44 @@ A complete service agreement and model release will be provided at your session
|
|||||||
bookingState.signatureDrawn = false;
|
bookingState.signatureDrawn = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initPaymentStep() {
|
||||||
|
const pricing = window.siteConfig?.booking?.pricing || { mini: 75, full: 150 };
|
||||||
|
const username = window.siteConfig?.booking?.venmoUsername || 'LisiLouPhoto';
|
||||||
|
const types = window.siteConfig?.booking?.sessionTypes || [];
|
||||||
|
const locations = window.siteConfig?.locations || [];
|
||||||
|
|
||||||
|
const amount = bookingState.sessionLength === 'mini' ? pricing.mini : pricing.full;
|
||||||
|
const typeLabel = (bookingState.sessionType === 'other' && bookingState.sessionTypeOther)
|
||||||
|
? `Other — ${bookingState.sessionTypeOther}`
|
||||||
|
: types.find(t => t.id === bookingState.sessionType)?.label || bookingState.sessionType;
|
||||||
|
const locLabel = locations.find(l => l.id === bookingState.sessionLocation)?.name || bookingState.sessionLocation;
|
||||||
|
const lenLabel = bookingState.sessionLength === 'mini' ? 'Mini — 30 min' : 'Full — 60 min';
|
||||||
|
|
||||||
|
document.getElementById('pay-summary-date').textContent = bookingState.sessionDate || '—';
|
||||||
|
document.getElementById('pay-summary-type').textContent = typeLabel || '—';
|
||||||
|
document.getElementById('pay-summary-length').textContent = lenLabel;
|
||||||
|
document.getElementById('pay-summary-location').textContent = locLabel || '—';
|
||||||
|
document.getElementById('payment-amount').textContent = `$${amount}`;
|
||||||
|
|
||||||
|
const note = `LisiLou Booking - ${typeLabel} on ${bookingState.sessionDate}`;
|
||||||
|
const venmoUrl = `https://venmo.com/${username}?txn=pay&amount=${amount}¬e=${encodeURIComponent(note)}`;
|
||||||
|
|
||||||
|
document.getElementById('venmo-link').href = venmoUrl;
|
||||||
|
renderVenmoQr(venmoUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderVenmoQr(venmoUrl) {
|
||||||
|
const container = document.getElementById('venmo-qr');
|
||||||
|
container.innerHTML = '';
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/venmo-qr?url=${encodeURIComponent(venmoUrl)}`);
|
||||||
|
if (!res.ok) throw new Error('qr failed');
|
||||||
|
container.innerHTML = await res.text();
|
||||||
|
} catch {
|
||||||
|
// Silent — the button still works without the QR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function openBooking() {
|
function openBooking() {
|
||||||
document.getElementById('booking-overlay').classList.add('active');
|
document.getElementById('booking-overlay').classList.add('active');
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
@@ -2050,8 +2163,8 @@ A complete service agreement and model release will be provided at your session
|
|||||||
btnNext.disabled = false;
|
btnNext.disabled = false;
|
||||||
counter.textContent = 'Step ' + step + ' of ' + BOOKING_STEPS;
|
counter.textContent = 'Step ' + step + ' of ' + BOOKING_STEPS;
|
||||||
|
|
||||||
if (step === BOOKING_STEPS) populateSummary();
|
if (step === 6) initContractStep();
|
||||||
if (step === 5) initContractStep();
|
if (step === 7) initPaymentStep();
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectOption(card, field) {
|
function selectOption(card, field) {
|
||||||
@@ -2115,6 +2228,25 @@ A complete service agreement and model release will be provided at your session
|
|||||||
case 4:
|
case 4:
|
||||||
return !!bookingState.sessionLocation;
|
return !!bookingState.sessionLocation;
|
||||||
case 5: {
|
case 5: {
|
||||||
|
// Your Info step
|
||||||
|
const name = document.getElementById('client-name').value.trim();
|
||||||
|
const email = document.getElementById('client-email').value.trim();
|
||||||
|
const errEl = document.getElementById('info-error');
|
||||||
|
if (!name || !email) {
|
||||||
|
errEl.textContent = 'Please enter your name and email address.';
|
||||||
|
errEl.style.display = 'block';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||||
|
errEl.textContent = 'Please enter a valid email address.';
|
||||||
|
errEl.style.display = 'block';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
errEl.style.display = 'none';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case 6: {
|
||||||
|
// Contract step
|
||||||
const errEl = document.getElementById('contract-error');
|
const errEl = document.getElementById('contract-error');
|
||||||
if (!bookingState.contractScrolled) {
|
if (!bookingState.contractScrolled) {
|
||||||
errEl.textContent = 'Please scroll through the full agreement.';
|
errEl.textContent = 'Please scroll through the full agreement.';
|
||||||
@@ -2126,8 +2258,8 @@ A complete service agreement and model release will be provided at your session
|
|||||||
errEl.style.display = 'block';
|
errEl.style.display = 'block';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const name = document.getElementById('contract-name').value.trim();
|
const sigName = document.getElementById('contract-name').value.trim();
|
||||||
if (!name) {
|
if (!sigName) {
|
||||||
errEl.textContent = 'Please type your full name to confirm.';
|
errEl.textContent = 'Please type your full name to confirm.';
|
||||||
errEl.style.display = 'block';
|
errEl.style.display = 'block';
|
||||||
document.getElementById('contract-name').focus();
|
document.getElementById('contract-name').focus();
|
||||||
@@ -2136,77 +2268,60 @@ A complete service agreement and model release will be provided at your session
|
|||||||
errEl.style.display = 'none';
|
errEl.style.display = 'none';
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
case 6: {
|
case 7: {
|
||||||
|
// Payment step — checkbox + submit
|
||||||
const ok = document.getElementById('payment-sent').checked;
|
const ok = document.getElementById('payment-sent').checked;
|
||||||
document.getElementById('payment-error').style.display = ok ? 'none' : 'block';
|
document.getElementById('payment-error').style.display = ok ? 'none' : 'block';
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
case 7:
|
|
||||||
return validateClientInfo();
|
|
||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateClientInfo() {
|
|
||||||
const name = document.getElementById('client-name').value.trim();
|
|
||||||
const email = document.getElementById('client-email').value.trim();
|
|
||||||
const errEl = document.getElementById('confirm-error');
|
|
||||||
if (!name || !email) {
|
|
||||||
errEl.textContent = 'Please enter your name and email address.';
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
||||||
errEl.textContent = 'Please enter a valid email address.';
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
errEl.style.display = 'none';
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitBooking() {
|
async function submitBooking() {
|
||||||
bookingState.sessionDate = document.getElementById('session-date').value;
|
// Client info is now collected in step 5
|
||||||
bookingState.clientName = document.getElementById('client-name').value.trim();
|
bookingState.sessionDate = document.getElementById('session-date').value;
|
||||||
bookingState.clientEmail = document.getElementById('client-email').value.trim();
|
bookingState.clientName = document.getElementById('client-name').value.trim();
|
||||||
bookingState.clientPhone = document.getElementById('client-phone').value.trim();
|
bookingState.clientEmail = document.getElementById('client-email').value.trim();
|
||||||
|
bookingState.clientPhone = document.getElementById('client-phone').value.trim();
|
||||||
|
|
||||||
const btnNext = document.getElementById('btn-next');
|
const btnNext = document.getElementById('btn-next');
|
||||||
btnNext.textContent = 'Submitting…';
|
btnNext.textContent = 'Submitting…';
|
||||||
btnNext.disabled = true;
|
btnNext.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Create booking record
|
// 1. Create booking — include payment_status since client confirmed sending
|
||||||
const res = await fetch('/api/bookings', {
|
const res = await fetch('/api/bookings', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
client_name: bookingState.clientName,
|
client_name: bookingState.clientName,
|
||||||
client_email: bookingState.clientEmail,
|
client_email: bookingState.clientEmail,
|
||||||
client_phone: bookingState.clientPhone,
|
client_phone: bookingState.clientPhone,
|
||||||
session_date: bookingState.sessionDate,
|
session_date: bookingState.sessionDate,
|
||||||
session_type: bookingState.sessionType === 'other'
|
session_type: bookingState.sessionType === 'other'
|
||||||
? 'other: ' + bookingState.sessionTypeOther
|
? 'other: ' + bookingState.sessionTypeOther
|
||||||
: bookingState.sessionType,
|
: bookingState.sessionType,
|
||||||
session_length: bookingState.sessionLength,
|
session_length: bookingState.sessionLength,
|
||||||
location: (window.siteConfig?.locations || []).find(l => l.id === bookingState.sessionLocation)?.name
|
location: (window.siteConfig?.locations || []).find(l => l.id === bookingState.sessionLocation)?.name
|
||||||
|| bookingState.sessionLocation,
|
|| bookingState.sessionLocation,
|
||||||
|
payment_status: 'pending_confirmation',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('server error');
|
if (!res.ok) throw new Error('server error');
|
||||||
const { id: bookingId } = await res.json();
|
const { id: bookingId } = await res.json();
|
||||||
|
|
||||||
// 2. Submit signature if collected in step 5
|
// 2. Submit signature captured in step 6
|
||||||
if (bookingState.signatureDrawn && bookingId) {
|
if (bookingState.signatureDrawn && bookingId) {
|
||||||
const sigCanvas = document.getElementById('sig-canvas');
|
const sigCanvas = document.getElementById('sig-canvas');
|
||||||
const contractName = document.getElementById('contract-name').value.trim();
|
const contractName = document.getElementById('contract-name').value.trim();
|
||||||
await fetch(`/api/bookings/${bookingId}/sign`, {
|
await fetch(`/api/bookings/${bookingId}/sign`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
signature_png: sigCanvas.toDataURL('image/png'),
|
signature_png: sigCanvas.toDataURL('image/png'),
|
||||||
full_name: contractName || bookingState.clientName,
|
full_name: contractName || bookingState.clientName,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user