diff --git a/package-lock.json b/package-lock.json index b4533a0..b3b7d86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "lucide-react": "^0.525.0", "next": "15.4.3", "next-themes": "^0.4.6", + "qrcode": "^1.5.4", "react": "19.1.0", "react-day-picker": "^9.8.1", "react-dom": "19.1.0", @@ -62,6 +63,7 @@ "recharts": "^2.15.4", "rehype-raw": "^7.0.0", "sonner": "^2.0.6", + "stripe": "^19.1.0", "tailwind-merge": "^3.3.1", "use-debounce": "^10.0.5", "zod": "^4.0.14" @@ -71,6 +73,7 @@ "@tailwindcss/postcss": "^4", "@types/leaflet": "^1.9.20", "@types/node": "^20", + "@types/qrcode": "^1.5.5", "@types/react": "^19", "@types/react-dom": "^19", "@types/react-resizable": "^3.0.8", @@ -2982,6 +2985,16 @@ "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", "license": "MIT" }, + "node_modules/@types/qrcode": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", + "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "19.1.13", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", @@ -4127,7 +4140,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4141,7 +4153,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4164,6 +4175,15 @@ "node": ">=6" } }, + "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/caniuse-lite": { "version": "1.0.30001743", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", @@ -4598,6 +4618,15 @@ } } }, + "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/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", @@ -4698,6 +4727,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "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/direction": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", @@ -4738,7 +4773,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -4922,7 +4956,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4932,7 +4965,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4970,7 +5002,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -5721,7 +5752,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5799,7 +5829,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5833,7 +5862,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -5976,7 +6004,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6068,7 +6095,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6097,7 +6123,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -7501,7 +7526,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8662,7 +8686,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8848,6 +8871,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8908,7 +8940,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8950,6 +8981,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "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/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", @@ -9066,6 +9106,156 @@ "node": ">=6" } }, + "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/qrcode/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/qrcode/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/qrcode/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/qrcode/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/qrcode/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/qrcode/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/qrcode/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/qrcode/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/qrcode/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" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9752,6 +9942,12 @@ "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/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -9935,6 +10131,12 @@ "node": ">=10" } }, + "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/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -10054,7 +10256,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -10074,7 +10275,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -10091,7 +10291,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -10110,7 +10309,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -10382,6 +10580,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-19.1.0.tgz", + "integrity": "sha512-FjgIiE98dMMTNssfdjMvFdD4eZyEzdWAOwPYqzhPRNZeg9ggFWlPXmX1iJKD5pPIwZBaPlC3SayQQkwsPo6/YQ==", + "license": "MIT", + "dependencies": { + "qs": "^6.11.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@types/node": ">=16" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", @@ -11194,6 +11412,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/which-typed-array": { "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", diff --git a/package.json b/package.json index edfd50e..d83a313 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "lucide-react": "^0.525.0", "next": "15.4.3", "next-themes": "^0.4.6", + "qrcode": "^1.5.4", "react": "19.1.0", "react-day-picker": "^9.8.1", "react-dom": "19.1.0", @@ -64,6 +65,7 @@ "recharts": "^2.15.4", "rehype-raw": "^7.0.0", "sonner": "^2.0.6", + "stripe": "^19.1.0", "tailwind-merge": "^3.3.1", "use-debounce": "^10.0.5", "zod": "^4.0.14" @@ -73,6 +75,7 @@ "@tailwindcss/postcss": "^4", "@types/leaflet": "^1.9.20", "@types/node": "^20", + "@types/qrcode": "^1.5.5", "@types/react": "^19", "@types/react-dom": "^19", "@types/react-resizable": "^3.0.8", diff --git a/src/app/(home-app)/_components/Topbar.tsx b/src/app/(home-app)/_components/Topbar.tsx index 3835952..01e2cfb 100644 --- a/src/app/(home-app)/_components/Topbar.tsx +++ b/src/app/(home-app)/_components/Topbar.tsx @@ -2,7 +2,7 @@ import Link from 'next/link' import {useAuth} from '@/providers/AuthProvider' -import {Ticket, ShieldCheck} from 'lucide-react' +import {Ticket, ShieldCheck, Receipt} from 'lucide-react' import {Button} from '@/components/ui/button' import { DropdownMenu, @@ -37,6 +37,14 @@ export default function Topbar() { {isAuthenticated ? ( <> + + + + + + + ); + } + + if (loading) { + return ( +
+
+ + +
+
+ {[1, 2, 3].map((i) => ( + + + + + + +
+ + + +
+
+
+ ))} +
+
+ ); + } + + if (error) { + return ( +
+
+

Error

+

{error}

+ +
+
+ ); + } + + const getStatusVariant = (status: string) => { + switch (status.toLowerCase()) { + case 'completed': + return 'default'; + case 'pending': + return 'secondary'; + case 'cancelled': + return 'destructive'; + case 'expired': + return 'outline'; + default: + return 'secondary'; + } + }; + + const getStatusColor = (status: string) => { + switch (status.toLowerCase()) { + case 'completed': + return 'text-white'; + case 'pending': + return 'text-amber-500'; + case 'cancelled': + return 'text-red-500'; + case 'expired': + return 'text-gray-500'; + default: + return 'text-gray-500'; + } + }; + + return ( +
+
+

+ + My Orders & Tickets +

+

+ View and manage all your event tickets and orders +

+
+ + {orders.length === 0 ? ( +
+ +

No Orders Yet

+

+ You haven't made any ticket purchases yet. Start exploring events! +

+ + + +
+ ) : ( +
+ {orders.map((order) => ( + + +
+
+ + Order #{order.OrderID.slice(-8)} + + {order.Status} + + +
+ + + {new Date(order.CreatedAt).toLocaleDateString()} + + + + Event: {order.EventID.slice(-8)} + +
+
+
+
+ ${order.Price.toFixed(2)} +
+
+ Total Amount +
+ {order.DiscountAmount && order.DiscountAmount > 0 && ( +
+ Saved ${order.DiscountAmount.toFixed(2)} +
+ )} +
+
+
+ +
+
+ +
+
Tickets
+
+ {order.tickets.length} ticket{order.tickets.length !== 1 ? 's' : ''} +
+
+
+
+ +
+
Session
+
+ {order.SessionID.slice(-8)} +
+
+
+
+ + {order.DiscountCode && ( +
+
+ Discount Applied: {order.DiscountCode} (-${order.DiscountAmount?.toFixed(2)}) +
+
+ )} + + {order.tickets.length > 0 && ( +
+
Tickets:
+
+ {order.tickets.map((ticket) => ( +
+
+
{ticket.seat_label}
+
{ticket.tier_name}
+
+
+
${ticket.price_at_purchase.toFixed(2)}
+
+ {ticket.checked_in ? 'Used' : 'Valid'} +
+
+
+ ))} +
+
+ )} + +
+ + + + + + + + + Order Details - #{order.OrderID.slice(-8)} + + + +
+ {/* Order Information */} +
+
+

Order Information

+
+
Order ID: {order.OrderID}
+
Status: + + {order.Status} + +
+
+
+ +
+

Event Information

+
+
Event ID: {order.EventID}
+
Session ID: {order.SessionID}
+
User ID: {order.UserID}
+
+
+
+ + {/* Pricing Information */} +
+

Pricing Breakdown

+
+
+ Subtotal: + ${order.SubTotal.toFixed(2)} +
+ {order.DiscountAmount && order.DiscountAmount > 0 && ( +
+ Discount ({order.DiscountCode}): + -${order.DiscountAmount.toFixed(2)} +
+ )} +
+ Total: + ${order.Price.toFixed(2)} +
+
+
+ + {/* Tickets Information */} +
+

Tickets ({order.tickets.length})

+
+ {order.tickets.map((ticket) => ( + + +
+
+

{ticket.seat_label}

+ {ticket.tier_name} +
+
+
${ticket.price_at_purchase.toFixed(2)}
+ + {ticket.checked_in ? 'Used' : 'Valid'} + +
+
+
+ +
+
+
Ticket ID:
+
{ticket.ticket_id.slice(-12)}
+
+
+
Seat ID:
+
{ticket.seat_id.slice(-12)}
+
+
+
Color:
+
+
+ {ticket.colour} +
+
+
+
Issued:
+
{new Date(ticket.issued_at).toLocaleDateString()}
+
+ {ticket.checked_in && ( +
+
Checked in:
+
{new Date(ticket.checked_in_time).toLocaleString()}
+
+ )} +
+ + {/* QR Code Button */} +
+ +
+
+
+ ))} +
+
+
+
+
+ + {order.Status.toLowerCase() === 'pending' && ( + + + + )} + {order.Status.toLowerCase() === 'completed' && ( + + )} +
+
+
+ ))} +
+ )} + + {/* QR Code Modal */} + { + setQrModalOpen(false); + setSelectedTicket(null); + }} + /> +
+ ); +} \ No newline at end of file diff --git a/src/components/ApiQRCodeModal.tsx b/src/components/ApiQRCodeModal.tsx new file mode 100644 index 0000000..ce66424 --- /dev/null +++ b/src/components/ApiQRCodeModal.tsx @@ -0,0 +1,171 @@ +'use client'; + +import { useState } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Download, Copy, CheckCircle, QrCode } from 'lucide-react'; +import { ApiTicket } from '@/types/order'; +import Image from 'next/image'; + +interface ApiQRCodeModalProps { + ticket: ApiTicket | null; + isOpen: boolean; + onClose: () => void; +} + +export const ApiQRCodeModal = ({ ticket, isOpen, onClose }: ApiQRCodeModalProps) => { + const [copied, setCopied] = useState(false); + + const handleCopyTicketId = async () => { + if (ticket) { + await navigator.clipboard.writeText(ticket.ticket_id); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; + + const handleDownloadQR = () => { + if (ticket && ticket.qr_code) { + try { + // Create a data URL from the base64 encoded QR code + const qrDataUrl = `data:image/png;base64,${ticket.qr_code}`; + + // Create a temporary link and download the image + const link = document.createElement('a'); + link.download = `ticket-${ticket.ticket_id.slice(-8)}-qr.png`; + link.href = qrDataUrl; + link.click(); + } catch (error) { + console.error('Error downloading QR code:', error); + } + } + }; + + if (!ticket) return null; + + // Create data URL for the QR code image + const qrCodeUrl = ticket.qr_code ? `data:image/png;base64,${ticket.qr_code}` : null; + + return ( + + + + + + Ticket QR Code + + + +
+ {/* Ticket Info */} +
+

Seat {ticket.seat_label}

+

{ticket.tier_name} Tier

+
+
+ {ticket.colour} +
+
+ Price: ${ticket.price_at_purchase.toFixed(2)} +
+
+ + {/* QR Code */} +
+ {qrCodeUrl ? ( +
+ Ticket QR Code +
+ ) : ( +
+ QR Code not available +
+ )} +
+ + {/* Ticket ID */} +
+ +
+ + {ticket.ticket_id} + + +
+
+ + {/* Status Info */} +
+
+ Status: + + {ticket.checked_in ? 'Used' : 'Valid'} + +
+
+ Issued: + {new Date(ticket.issued_at).toLocaleDateString()} +
+ {ticket.checked_in && ticket.checked_in_time !== "0001-01-01T00:00:00Z" && ( +
+ Checked in: + {new Date(ticket.checked_in_time).toLocaleString()} +
+ )} +
+ + {/* Actions */} +
+ + +
+ + {/* Instructions */} +
+

Present this QR code at the event entrance

+

Keep this ticket until after the event

+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/OrderDebugView.tsx b/src/components/OrderDebugView.tsx new file mode 100644 index 0000000..d92f170 --- /dev/null +++ b/src/components/OrderDebugView.tsx @@ -0,0 +1,142 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { ChevronDown, ChevronUp, Code } from 'lucide-react'; + +// Use a safer approach with explicit type casting +interface OrderDebugViewProps { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + order: any; // Using any here is justified for a debugging component + orderId: string; +} + +export const OrderDebugView = ({ order, orderId }: OrderDebugViewProps) => { + const [isExpanded, setIsExpanded] = useState(false); + + if (!order) { + return ( + + + +
+ + Order Data +
+ No Data +
+
+ +

No order data available for ID: {orderId}

+
+
+ ); + } + + return ( + + + +
+ + Order Data +
+ +
+
+ {isExpanded && ( + +
+
+              {JSON.stringify(order, null, 2)}
+            
+
+
+ )} + +
+
+ Order ID: +
{String(order.OrderID || orderId)}
+
+
+ Status: +
+ {order.Status ? String(order.Status) : 'Unknown'} +
+
+
+ User: +
{order.UserID ? String(order.UserID) : 'N/A'}
+
+
+ Amount: +
+ {typeof order.Price === 'number' + ? `$${order.Price.toFixed(2)}` + : 'N/A'} +
+
+
+ Event: +
{order.EventID ? String(order.EventID) : 'N/A'}
+
+
+ Session: +
{order.SessionID ? String(order.SessionID) : 'N/A'}
+
+ {order.CreatedAt && typeof order.CreatedAt === 'string' && ( +
+ Created: +
+ {new Date(order.CreatedAt).toLocaleString()} +
+
+ )} + {order.SeatIDs && ( +
+ Seats: +
+ {Array.isArray(order.SeatIDs) ? + // eslint-disable-next-line @typescript-eslint/no-explicit-any + order.SeatIDs.map((seat: any) => String(seat)).join(', ') : + 'N/A'} +
+
+ )} +
+
+
+ ); +}; + +// Helper function to get color based on status +function getStatusColor(status: unknown): string { + if (!status || typeof status !== 'string') return 'text-gray-500'; + + switch(status.toLowerCase()) { + case 'completed': + return 'text-white'; + case 'pending': + return 'text-amber-500'; + case 'cancelled': + return 'text-red-500'; + case 'expired': + return 'text-gray-500'; + default: + return 'text-gray-500'; + } +} \ No newline at end of file diff --git a/src/components/PaymentForm.tsx b/src/components/PaymentForm.tsx new file mode 100644 index 0000000..635b4a9 --- /dev/null +++ b/src/components/PaymentForm.tsx @@ -0,0 +1,133 @@ +'use client'; + +import { useState } from 'react'; +import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Loader2 } from 'lucide-react'; + +interface PaymentFormProps { + amount: number; // Amount in cents + orderId: string; +} + +const PaymentForm = ({ amount }: PaymentFormProps) => { + const stripe = useStripe(); + const elements = useElements(); + + const [error, setError] = useState(null); + const [cardComplete, setCardComplete] = useState(false); + const [processing, setProcessing] = useState(false); + const [paymentSuccess] = useState(false); + const [billingDetails, setBillingDetails] = useState({ + name: '', + email: '', + }); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + if (!stripe || !elements) { + // Stripe.js hasn't loaded yet + return; + } + + if (!cardComplete) { + setError('Please complete your card details.'); + return; + } + + if (!billingDetails.name || !billingDetails.email) { + setError('Please provide your name and email.'); + return; + } + + setProcessing(true); + setError(null); + + try { + await new Promise((resolve) => setTimeout(resolve, 10000)); + } catch (err) { + setError('An error occurred while processing your payment. Please try again.'); + console.error(err); + } + + setProcessing(false); + }; + + return ( +
+
+ + setBillingDetails({ ...billingDetails, name: e.target.value })} + /> +
+ +
+ + setBillingDetails({ ...billingDetails, email: e.target.value })} + /> +
+ +
+ +
+ setCardComplete(e.complete)} + /> +
+
+ + {error && ( +
{error}
+ )} + + +
+ ); +}; + +export default PaymentForm; diff --git a/src/components/QRCodeModal.tsx b/src/components/QRCodeModal.tsx new file mode 100644 index 0000000..8f296e5 --- /dev/null +++ b/src/components/QRCodeModal.tsx @@ -0,0 +1,168 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Download, Copy, CheckCircle } from 'lucide-react'; +import QRCode from 'qrcode'; +import { Ticket } from '@/types/order'; +import Image from 'next/image'; + +interface QRCodeModalProps { + ticket: Ticket | null; + isOpen: boolean; + onClose: () => void; +} + +export const QRCodeModal = ({ ticket, isOpen, onClose }: QRCodeModalProps) => { + const [qrCodeUrl, setQrCodeUrl] = useState(''); + const [copied, setCopied] = useState(false); + + useEffect(() => { + if (ticket && isOpen) { + // Generate QR code with ticket information + const ticketData = { + ticketId: ticket.TicketID, + eventName: ticket.EventName, + seatId: ticket.SeatID, + eventId: ticket.EventID, + sessionId: ticket.SessionID, + validUntil: ticket.ValidUntil, + qrCode: ticket.QRCode + }; + + QRCode.toDataURL(JSON.stringify(ticketData), { + width: 300, + margin: 2, + color: { + dark: '#000000', + light: '#FFFFFF' + } + }) + .then(url => { + setQrCodeUrl(url); + }) + .catch(err => { + console.error('Error generating QR code:', err); + }); + } + }, [ticket, isOpen]); + + const handleCopyTicketId = async () => { + if (ticket) { + await navigator.clipboard.writeText(ticket.TicketID); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; + + const handleDownloadQR = () => { + if (qrCodeUrl && ticket) { + const link = document.createElement('a'); + link.download = `ticket-${ticket.TicketID.slice(-8)}-qr.png`; + link.href = qrCodeUrl; + link.click(); + } + }; + + if (!ticket) return null; + + return ( + + + + + Ticket QR Code + + + +
+ {/* Ticket Info */} +
+

{ticket.EventName}

+

{ticket.VenueName}

+
+ {ticket.SessionDate ? new Date(ticket.SessionDate).toLocaleDateString() : ''} + {ticket.SessionTime} +
+
+ Seat: {ticket.SeatRow ? `${ticket.SeatRow}${ticket.SeatNumber}` : ticket.SeatID} +
+
+ + {/* QR Code */} +
+ {qrCodeUrl ? ( +
+ Ticket QR Code +
+ ) : ( +
+ Generating QR Code... +
+ )} +
+ + {/* Ticket ID */} +
+ +
+ + {ticket.TicketID} + + +
+
+ + {/* Actions */} +
+ + +
+ + {/* Instructions */} +
+

Present this QR code at the event entrance

+

Valid until: {new Date(ticket.ValidUntil).toLocaleDateString()}

+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/TicketCard.tsx b/src/components/TicketCard.tsx new file mode 100644 index 0000000..65ac9dc --- /dev/null +++ b/src/components/TicketCard.tsx @@ -0,0 +1,297 @@ +'use client'; + +import { useState } from 'react'; +import { Ticket, TicketStatus } from '@/types/order'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + QrCode, + Calendar, + MapPin, + Clock, + Users, + Ticket as TicketIcon, + ChevronDown, + ChevronUp, + Download, + Eye +} from 'lucide-react'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { QRCodeModal } from './QRCodeModal'; +import { generateTicketPDF, downloadAllTickets } from '@/lib/ticketPDF'; + +interface TicketCardProps { + ticket: Ticket; +} + +interface TicketsListProps { + tickets: Ticket[]; + orderStatus: string; +} + +export const TicketCard = ({ ticket }: TicketCardProps) => { + const [qrModalOpen, setQrModalOpen] = useState(false); + + const getTicketStatusVariant = (status: TicketStatus) => { + switch (status) { + case TicketStatus.VALID: + return 'default'; + case TicketStatus.USED: + return 'secondary'; + case TicketStatus.EXPIRED: + return 'outline'; + case TicketStatus.CANCELLED: + return 'destructive'; + default: + return 'secondary'; + } + }; + + const getTicketStatusColor = (status: TicketStatus) => { + switch (status) { + case TicketStatus.VALID: + return 'text-white'; + case TicketStatus.USED: + return 'text-blue-600'; + case TicketStatus.EXPIRED: + return 'text-gray-500'; + case TicketStatus.CANCELLED: + return 'text-red-500'; + default: + return 'text-gray-500'; + } + }; + + return ( + + +
+
+ +
+ + Ticket #{ticket.TicketID.slice(-8)} + +
+ + {ticket.Status} + + {ticket.TicketType && ( + {ticket.TicketType} + )} +
+
+
+
+
${ticket.Price.toFixed(2)}
+
Ticket Price
+
+
+
+ +
+
+
+ +
+
Seat
+
+ {ticket.SeatRow ? `Row ${ticket.SeatRow}, ` : ''} + Seat {ticket.SeatNumber || ticket.SeatID} +
+
+
+ + {ticket.EventName && ( +
+ +
+
Event
+
{ticket.EventName}
+
+
+ )} + + {ticket.VenueName && ( +
+ +
+
Venue
+
{ticket.VenueName}
+
+
+ )} +
+ +
+ {ticket.SessionDate && ( +
+ +
+
Date
+
+ {new Date(ticket.SessionDate).toLocaleDateString()} +
+
+
+ )} + + {ticket.SessionTime && ( +
+ +
+
Time
+
{ticket.SessionTime}
+
+
+ )} + +
+ +
+
Valid Until
+
+ {new Date(ticket.ValidUntil).toLocaleDateString()} +
+
+
+
+
+ + {/* QR Code and Actions */} +
+
+
+ + + QR: {ticket.QRCode.slice(-12)} + +
+ +
+ {ticket.Status === TicketStatus.VALID && ( + <> + + + + )} + {ticket.Status === TicketStatus.USED && ( + + )} +
+
+
+
+ + {/* QR Code Modal */} + setQrModalOpen(false)} + /> +
+ ); +}; + +export const TicketsList = ({ tickets, orderStatus }: TicketsListProps) => { + const [isOpen, setIsOpen] = useState(false); + + const validTickets = tickets.filter(ticket => ticket.Status === TicketStatus.VALID); + + if (tickets.length === 0) { + return null; + } + + return ( +
+ +
+ + + + + {orderStatus === 'completed' && validTickets.length > 1 && ( + + )} +
+ + +
+ {orderStatus === 'completed' + ? 'Your tickets are ready to use' + : `Tickets will be available when order is completed`} +
+ + {orderStatus === 'completed' ? ( +
+ {tickets.map((ticket) => ( + + ))} +
+ ) : ( +
+ {tickets.map((ticket, index) => ( +
+ +
+
+ Ticket #{index + 1} - Seat {ticket.SeatID} +
+
+ Will be available after payment completion +
+
+
+ ))} +
+ )} +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/lib/actions/orderActions.tsx b/src/lib/actions/orderActions.tsx index 84dc706..6f116a9 100644 --- a/src/lib/actions/orderActions.tsx +++ b/src/lib/actions/orderActions.tsx @@ -1,5 +1,5 @@ import { apiFetch } from "@/lib/api"; -import { CreateOrderRequest, CreateOrderResponse } from "@/types/order"; +import {ApiOrder, CreateOrderRequest, CreateOrderResponse, Order} from "@/types/order"; import { OrderDetailsResponse } from "./analyticsActions"; const API_BASE_PATH = '/order'; @@ -22,4 +22,22 @@ export const createPaymentIntent = (orderId: string): Promise(`${API_BASE_PATH}/${orderId}/create-payment-intent`, { method: 'POST', }); -} \ No newline at end of file +} + +export const fetchOrderById = (orderId: string): Promise => { + return apiFetch(`${API_BASE_PATH}/${orderId}`, { + method: 'GET', + }); +}; + +export const fetchUserOrders = (): Promise => { + return apiFetch(`${API_BASE_PATH}/user/orders`, { + method: 'GET', + }); +}; + +export const fetchOrdersByUserId = (userId: string): Promise => { + return apiFetch(`${API_BASE_PATH}/user/${userId}`, { + method: 'GET', + }); +}; diff --git a/src/lib/stripe.ts b/src/lib/stripe.ts new file mode 100644 index 0000000..415c479 --- /dev/null +++ b/src/lib/stripe.ts @@ -0,0 +1,7 @@ +import { loadStripe } from '@stripe/stripe-js'; + +// Make sure to call `loadStripe` outside of a component's render to avoid +// recreating the `Stripe` object on every render +export const stripePromise = loadStripe( + process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY! +); diff --git a/src/lib/ticketPDF.ts b/src/lib/ticketPDF.ts new file mode 100644 index 0000000..439fe8e --- /dev/null +++ b/src/lib/ticketPDF.ts @@ -0,0 +1,210 @@ +import { Ticket } from '@/types/order'; +import QRCode from 'qrcode'; + +export const generateTicketPDF = async (ticket: Ticket): Promise => { + try { + // Generate QR code + const ticketData = { + ticketId: ticket.TicketID, + eventName: ticket.EventName, + seatId: ticket.SeatID, + eventId: ticket.EventID, + sessionId: ticket.SessionID, + validUntil: ticket.ValidUntil, + qrCode: ticket.QRCode + }; + + const qrCodeDataUrl = await QRCode.toDataURL(JSON.stringify(ticketData), { + width: 200, + margin: 2, + color: { + dark: '#000000', + light: '#FFFFFF' + } + }); + + // Create a simple HTML ticket template + const ticketHTML = ` + + + + + Ticket - ${ticket.EventName} + + + +
+
+

${ticket.EventName || 'Event'}

+

${ticket.VenueName || 'Venue'}

+
+ +
+
+
+ Date: + ${ticket.SessionDate ? new Date(ticket.SessionDate).toLocaleDateString() : 'TBD'} +
+
+ Time: + ${ticket.SessionTime || 'TBD'} +
+
+ Seat: + ${ticket.SeatRow ? `Row ${ticket.SeatRow}, ` : ''}Seat ${ticket.SeatNumber || ticket.SeatID} +
+
+ Type: + ${ticket.TicketType || 'Standard'} +
+
+ Price: + $${ticket.Price.toFixed(2)} +
+
+ Valid Until: + ${new Date(ticket.ValidUntil).toLocaleDateString()} +
+
+ +
+ QR Code +
+ Present this QR code
+ at the event entrance +
+
+
+ + +
+ + + `; + + // Create a new window with the ticket HTML + const printWindow = window.open('', '_blank'); + if (printWindow) { + printWindow.document.write(ticketHTML); + printWindow.document.close(); + + // Wait for images to load then trigger print + printWindow.onload = () => { + setTimeout(() => { + printWindow.print(); + printWindow.close(); + }, 500); + }; + } + } catch (error) { + console.error('Error generating ticket PDF:', error); + alert('Error generating ticket. Please try again.'); + } +}; + +export const downloadAllTickets = async (tickets: Ticket[]): Promise => { + try { + for (let i = 0; i < tickets.length; i++) { + await generateTicketPDF(tickets[i]); + // Add a small delay between downloads to prevent browser blocking + if (i < tickets.length - 1) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + } catch (error) { + console.error('Error downloading all tickets:', error); + alert('Error downloading tickets. Please try again.'); + } +}; \ No newline at end of file diff --git a/src/types/order.ts b/src/types/order.ts index d8249b8..08e8073 100644 --- a/src/types/order.ts +++ b/src/types/order.ts @@ -48,3 +48,75 @@ export interface OrderDetailsResponse { TotalCount?: number; // Total count for pagination tickets: TicketResponse[]; } + +export interface ApiTicket { + ticket_id: string; + order_id: string; + seat_id: string; + seat_label: string; + colour: string; + tier_id: string; + tier_name: string; + qr_code: string; // Base64 encoded QR code data + price_at_purchase: number; + issued_at: string; + checked_in: boolean; + checked_in_time: string; +} + +export interface ApiOrder { + OrderID: string; + UserID: string; + EventID: string; + SessionID: string; + Status: string; + SubTotal: number; + DiscountID?: string; + DiscountCode?: string; + DiscountAmount?: number; + Price: number; + CreatedAt: string; + tickets: ApiTicket[]; +} + + +export interface Order { + OrderID: string; + UserID: string; + SessionID: string; + SeatIDs: string[]; + Status: string; + Price: number; + CreatedAt: string; + EventID?: string; // May be included in future responses + UpdatedAt?: string; + DiscountID?: string; + PaymentID?: string; +} + +export enum TicketStatus { + VALID = "VALID", + USED = "USED", + EXPIRED = "EXPIRED", + CANCELLED = "CANCELLED" +} + +export interface Ticket { + TicketID: string; + OrderID: string; + SeatID: string; + EventID: string; + SessionID: string; + QRCode: string; + Status: TicketStatus; + IssuedAt: string; + ValidUntil: string; + EventName?: string; + VenueName?: string; + SessionDate?: string; + SessionTime?: string; + SeatRow?: string; + SeatNumber?: string; + TicketType?: string; + Price: number; +} \ No newline at end of file