diff --git a/eslint.config.mjs b/eslint.config.mjs index 719cea2..42817f9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,25 +1,14 @@ -import { dirname } from "path"; -import { fileURLToPath } from "url"; -import { FlatCompat } from "@eslint/eslintrc"; +import nextCoreWebVitals from 'eslint-config-next/core-web-vitals'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const compat = new FlatCompat({ - baseDirectory: __dirname, -}); - -const eslintConfig = [ - ...compat.extends("next/core-web-vitals", "next/typescript"), +export default [ + ...nextCoreWebVitals, { ignores: [ - "node_modules/**", - ".next/**", - "out/**", - "build/**", - "next-env.d.ts", + 'node_modules/**', + '.next/**', + 'out/**', + 'build/**', + 'next-env.d.ts', ], }, ]; - -export default eslintConfig; diff --git a/package-lock.json b/package-lock.json index 400e880..b55e904 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,12 +21,15 @@ "@tailwindplus/elements": "^1.0.22", "@tailwindui/react": "^0.1.1", "axios": "^1.13.5", + "canvg": "^4.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "country-flag-icons": "^1.6.13", "country-select-js": "^2.1.0", "gsap": "^3.14.2", + "html2canvas": "^1.4.1", "intl-tel-input": "^26.4.1", + "jspdf": "^4.2.0", "lucide-react": "^0.574.0", "motion": "^12.34.1", "next": "^16.1.6", @@ -48,14 +51,14 @@ }, "devDependencies": { "@eslint/eslintrc": "^3", - "@eslint/js": "^10.0.1", + "@eslint/js": "^9.0.1", "@tailwindcss/postcss": "^4", "@types/node": "^25", "@types/react": "^19", "@types/react-dom": "^19", "autoprefixer": "^10.4.24", "baseline-browser-mapping": "^2.9.19", - "eslint": "^10.0.0", + "eslint": "^9.0.0", "eslint-config-next": "^16.1.6", "eslint-plugin-react-hooks": "^7.0.1", "globals": "^17.3.0", @@ -556,20 +559,6 @@ "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@csstools/postcss-color-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-5.0.1.tgz", @@ -970,20 +959,6 @@ "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@csstools/postcss-light-dark-function": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-3.0.0.tgz", @@ -1462,20 +1437,6 @@ "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@csstools/postcss-sign-functions": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-2.0.0.tgz", @@ -1825,103 +1786,61 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.1.tgz", - "integrity": "sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.1", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^10.1.1" + "minimatch": "^3.1.5" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-array/node_modules/balanced-match": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz", - "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==", - "dev": true, - "license": "MIT", - "dependencies": { - "jackspeak": "^4.2.3" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz", - "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", - "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.0" + "@eslint/core": "^0.17.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", - "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -1945,48 +1864,40 @@ } }, "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } } }, "node_modules/@eslint/object-schema": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.1.tgz", - "integrity": "sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", - "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@floating-ui/core": { @@ -2611,16 +2522,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@isaacs/cliui": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", - "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -3842,6 +3743,19 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@tailwindplus/elements": { "version": "1.0.22", "resolved": "https://registry.npmjs.org/@tailwindplus/elements/-/elements-1.0.22.tgz", @@ -3910,13 +3824,6 @@ "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", "license": "MIT" }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3954,11 +3861,22 @@ "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", "license": "MIT" }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -4010,6 +3928,13 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/webxr": { "version": "0.5.24", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", @@ -4215,13 +4140,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -4608,9 +4533,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -4624,6 +4549,35 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansi-styles/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -4921,6 +4875,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -5151,6 +5114,39 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-4.0.3.tgz", + "integrity": "sha512-fKzMoMBwus3CWo1Uy8XJc4tqqn98RoRrGV6CsIkaNiQT5lOeHuMh4fOt+LXLzn2Wqtr4p/c2TOLz4xtu4oBlFA==", + "license": "MIT", + "dependencies": { + "@types/raf": "^3.4.0", + "raf": "^3.4.1", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -5290,6 +5286,18 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", + "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/country-flag-icons": { "version": "1.6.13", "resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.6.13.tgz", @@ -5366,20 +5374,6 @@ "postcss": "^8.4" } }, - "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/css-has-pseudo": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-8.0.0.tgz", @@ -5408,18 +5402,13 @@ "postcss": "^8.4" } }, - "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", "license": "MIT", "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" + "utrie": "^1.0.2" } }, "node_modules/css-prefers-color-scheme": { @@ -5658,6 +5647,16 @@ "node": ">=0.10.0" } }, + "node_modules/dompurify": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", + "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/draco3d": { "version": "1.5.7", "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", @@ -5915,30 +5914,33 @@ } }, "node_modules/eslint": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.0.tgz", - "integrity": "sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.0", - "@eslint/config-helpers": "^0.5.2", - "@eslint/core": "^1.1.0", - "@eslint/plugin-kit": "^0.6.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", + "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.0", - "eslint-visitor-keys": "^5.0.0", - "espree": "^11.1.0", - "esquery": "^1.7.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -5948,7 +5950,8 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.1.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -5956,7 +5959,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://eslint.org/donate" @@ -6247,19 +6250,17 @@ } }, "node_modules/eslint-scope": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.0.tgz", - "integrity": "sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6278,66 +6279,19 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/balanced-match": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz", - "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==", + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "license": "MIT", - "dependencies": { - "jackspeak": "^4.2.3" - }, + "license": "Apache-2.0", "engines": { - "node": "20 || >=22" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/eslint/node_modules/espree": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.0.tgz", - "integrity": "sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz", - "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -6474,6 +6428,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, + "node_modules/fast-png/node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -6572,9 +6543,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", "dev": true, "license": "ISC" }, @@ -6918,6 +6889,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -7024,6 +7005,19 @@ "integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==", "license": "ISC" }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/hyphen": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.14.1.tgz", @@ -7144,6 +7138,12 @@ "site" ] }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -7632,22 +7632,6 @@ "react": "^19.0.0" } }, - "node_modules/jackspeak": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", - "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^9.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/jay-peg": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/jay-peg/-/jay-peg-1.1.1.tgz", @@ -7733,6 +7717,43 @@ "node": ">=6" } }, + "node_modules/jspdf": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.0.tgz", + "integrity": "sha512-hR/hnRevAXXlrjeqU5oahOE+Ln9ORJUB5brLHHqH67A+RBQZuFr5GkbI9XQI8OUFSEezKegsi45QRpc4bGj75Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jspdf/node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -8110,6 +8131,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/logform": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", @@ -8273,6 +8301,19 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -8304,9 +8345,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -8815,6 +8856,12 @@ "node-readable-to-web-readable-stream": "^0.4.2" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -8822,13 +8869,13 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -8899,20 +8946,6 @@ "postcss": "^8.4" } }, - "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-clamp": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", @@ -9101,20 +9134,6 @@ "postcss": "^8.4" } }, - "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-dir-pseudo-class": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-10.0.0.tgz", @@ -9141,20 +9160,6 @@ "postcss": "^8.4" } }, - "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-double-position-gradients": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-7.0.0.tgz", @@ -9209,20 +9214,6 @@ "postcss": "^8.4" } }, - "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-focus-within": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-10.0.0.tgz", @@ -9249,20 +9240,6 @@ "postcss": "^8.4" } }, - "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-font-variant": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", @@ -9407,20 +9384,6 @@ "postcss": "^8.4" } }, - "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-opacity-percentage": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz", @@ -9629,20 +9592,6 @@ "postcss": "^8.4" } }, - "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-replace-overflow-wrap": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", @@ -9679,7 +9628,7 @@ "postcss": "^8.4" } }, - "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { + "node_modules/postcss-selector-parser": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", @@ -9693,19 +9642,6 @@ "node": ">=4" } }, - "node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", @@ -9801,6 +9737,15 @@ ], "license": "MIT" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -10004,6 +9949,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -10092,6 +10044,15 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10454,6 +10415,15 @@ "node": "*" } }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/stats-gl": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", @@ -10662,6 +10632,19 @@ } } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -10690,6 +10673,15 @@ "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", "license": "ISC" }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/tabbable": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", @@ -10710,7 +10702,6 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "dev": true, "license": "MIT" }, "node_modules/tailwindcss-animate": { @@ -10742,6 +10733,15 @@ "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/three": { "version": "0.182.0", "resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz", @@ -10815,19 +10815,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -11261,6 +11248,15 @@ "node": ">= 4" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/vite-compatible-readable-stream": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz", diff --git a/package.json b/package.json index 364be82..0a5c2b0 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,15 @@ "@tailwindplus/elements": "^1.0.22", "@tailwindui/react": "^0.1.1", "axios": "^1.13.5", + "canvg": "^4.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "country-flag-icons": "^1.6.13", "country-select-js": "^2.1.0", "gsap": "^3.14.2", + "html2canvas": "^1.4.1", "intl-tel-input": "^26.4.1", + "jspdf": "^4.2.0", "lucide-react": "^0.574.0", "motion": "^12.34.1", "next": "^16.1.6", @@ -65,4 +68,4 @@ "tailwindcss": "^4.1.18", "typescript": "^5" } -} \ No newline at end of file +} diff --git a/public/templates/abo-contract-template.html b/public/templates/abo-contract-template.html new file mode 100644 index 0000000..34f5747 --- /dev/null +++ b/public/templates/abo-contract-template.html @@ -0,0 +1,694 @@ + + + + + + ABO Vertrag – Profit Planet GmbH + + + +
+
+
+

ABO Vertrag

+

Angebot auf Abschluss eines Kauf- Mietvertrages Kaffee-Service- Kapsel

+
+
+
PROFIT PLANET GMBH
+
Liebenauer Hauptstraße 82c
+
A-8041 Graz
+
FN-649474 i
+
IBAN: AT16 2081 5000 4639 9507
+
Swift/BIC Code: STSPAT2GXXX
+
ATU82089605
+
+
+ +
+
+

Vertrag über automatische Wiederbestellungen (ABO)

+

Bitte alle Felder vollständig ausfüllen und Zutreffendes ankreuzen.

+
+
+
Vertragsnummer:
+
{{contractNumber}}
+
Datum:
+
{{currentDate}}
+
+
+ +

An die

+
+
+
Empfänger
+
{{recipientName}}
+
+
+
Adresse
+
{{recipientAddress}}
+
+
+ +

Lieferadresse

+
+
+
+
+ KUNDE + FIRMA +
+
Vor- und Nachname
{{shippingFullName}}
+
Adresse
{{shippingStreet}}
+
PLZ / Ort
{{shippingPostalCode}}    {{shippingCity}}
+
+
+
Telefonnummer
{{shippingPhone}}
+
Mobil
{{shippingMobile}}
+
E-Mail-Adresse
{{shippingEmail}}
+
+
+
+
+ +
Rechnungsadresse: {{invoiceSameAsShippingMark}} wie Lieferadresse
+
+ +
+ +

Rechnungsadresse (falls abweichend)

+
+
+
+
+ FIRMA + KUNDE +
+
Vor- und Nachname
{{invoiceFullName}}
+
Adresse
{{invoiceStreet}}
+
PLZ / Ort
{{invoicePostalCode}}    {{invoiceCity}}
+
+
+
Telefonnummer
{{invoicePhone}}
+
Mobil
{{invoiceMobile}}
+
E-Mail-Adresse
{{invoiceEmail}}
+
+
+ +
+ FN: {{fnNumber}} + ATU: {{atuNumber}} +
+
+ +

Zutreffendes bitte ankreuzen

+
+
+ + Der Kunde/Käufer tätigt das gegenständliche Rechtsgeschäft als Unternehmer im Sinne des § 1 Abs 1 Z 1 KSchG, das heißt, das Geschäft gehört zum Betrieb seines Unternehmens. +
+
+ + Der Kunde/Käufer tätigt das gegenständliche Rechtsgeschäft als Konsument im Sinne des § 1 Abs 1 Z 2 KSchG. +
+
+ +

Angebote

+
+

+ Mindestbestellmenge für BIO Kaffee und BIO Tee und BIO Kakao beträgt pro Bestellung jeweils 120 Kapseln. + Preis pro Kapsel € 2,97 inkl. 20% MwSt. Preise und Konditionen gemäß gültigem PROFIT PLANET GMBH Tarif. +

+ + + + + + + + + + + + + + + + + +
TarifPreis pro Kapsel
Customer without abo2.97€
Customer with abo1.77€
+
+ +

Produktauswahl

+
+

Superfood Coffee – 60 Kapseln (bitte gewünschte Sorten ankreuzen / ergänzen)

+ {{selectedProductsHtml}} + +

+ Bei Angabe einer automatischen Wiederbestellung, gemäß den Regelungen in nachstehendem Punkt 3, erhält der Kunde in regelmäßigen Abständen, + BEGINNEND AM (Unterzeichnung des Vertrages) vorstehend eingetragene BIO Kaffee-Teemenge für die Dauer des Vertrages oder bis zum Widerruf der automatischen Wiederbestellung. + Der BIO Kaffee-Tee wird automatisch im monatlichen Abstand fakturiert und innerhalb von drei bis fünf Werktagen an den Kunden geliefert.

+
+ +
+ +

Zahlungsart

+
+
+ Sepa + Kreditkarte + Sofortbanking +
+
+ Bitte senden Sie mir meine Rechnung per E-Mail zu! +
+
+ + + +
+ + + +
+ + + +
+ + + +
+ + +
+ + +
+ + + +

§ 7 Sachmängel

+

(1) In dringenden Fällen, z.B. bei Gefährdung der Betriebssicherheit oder zur Abwehr unverhältnismäßiger Schäden, hat der Käufer das Recht, den Mangel selbst zu beseitigen und von uns Ersatz der hierzu objektiv erforderlichen (angemessenen) Aufwendungen zu verlangen. Von einer derartigen Selbstvornahme sind wir unverzüglich – nach Möglichkeit vorher – zu benachrichtigen. Das Selbstvornahmerecht besteht nicht, wenn wir berechtigt wären, eine entsprechende Nacherfüllung nach den gesetzlichen Vorschriften zu verweigern.

+

(2) Wenn die Nacherfüllung fehlgeschlagen ist oder eine für die Nacherfüllung vom Käufer zu setzende angemessene Frist erfolglos abgelaufen oder nach den gesetzlichen Vorschriften entbehrlich ist, kann der Käufer vom Kaufvertrag zurücktreten oder den Kaufpreis mindern. Bei einem unerheblichen Mangel besteht jedoch kein Rücktrittsrecht. Bei Verbrauchern gelten unbeschadet vorstehender Regelungen die gesetzlichen Gewährleistungsrechte uneingeschränkt. Insbesondere beträgt die Gewährleistungsfrist für Verbraucher zwei Jahre ab Übergabe der Ware.

+

(3) Ansprüche des Käufers auf Schadensersatz bzw. Ersatz vergeblicher Aufwendungen bestehen auch bei Mängeln nur nach Maßgabe von § 8 und sind im Übrigen ausgeschlossen.

+ +

§ 8 Sonstige Haftung

+

(1) Soweit sich aus diesen AGB einschließlich der nachfolgenden Bestimmungen nichts anderes ergibt, haften wir bei der Verletzung vertraglicher und außervertraglicher Pflichten nach den gesetzlichen Vorschriften.

+

(2) Wir haften – gleich aus welchem Rechtsgrund – im Rahmen der Verschuldenshaftung bei Vorsatz und grober Fahrlässigkeit. Bei leichter Fahrlässigkeit haften wir nur a) für Schäden aus der Verletzung des Lebens, des Körpers oder der Gesundheit, b) für Schäden aus der Verletzung wesentlicher Vertragspflichten (d. h. solcher Pflichten, deren Erfüllung die ordnungsgemäße Durchführung des Vertrags überhaupt erst ermöglicht und auf deren Einhaltung der Vertragspartner regelmäßig vertrauen darf). In diesem Fall ist unsere Haftung jedoch auf den Ersatz des typischen, vorhersehbaren Schadens begrenzt.

+

(3) Die vorstehenden Haftungsbeschränkungen gelten auch zugunsten Dritter sowie für Pflichtverletzungen durch Personen, deren Verschulden uns nach den gesetzlichen Vorschriften zuzurechnen ist. Sie gelten nicht, soweit wir einen Mangel arglistig verschwiegen oder eine Garantie für die Beschaffenheit der Ware übernommen haben sowie bei Ansprüchen nach dem Produkthaftungsgesetz.

+

(4) Soweit gesetzlich zulässig, haften wir nicht für mittelbare Schäden, Folgeschäden oder entgangenen Gewinn. Gegenüber Verbrauchern gilt dieser Haftungsausschluss nicht, soweit ein kausaler Zusammenhang mit der Verletzung wesentlicher Vertragspflichten besteht.

+

(5) Ein Rücktritt oder eine Kündigung wegen Pflichtverletzung, die nicht auf einem Mangel der Ware beruht, ist nur zulässig, wenn wir diese zu vertreten haben. Ein darüber hinausgehendes freies Rücktritts- oder Kündigungsrecht des Käufers wird – soweit rechtlich zulässig – ausgeschlossen.

+

(6) Die zwingenden Bestimmungen des Produkthaftungsgesetzes sowie die Haftung für vorsätzliches oder grob fahrlässiges Verhalten bleiben von den vorstehenden Regelungen unberührt.

+ +

§ 9 Verjährung

+

(1) Bei Verträgen mit Unternehmern im Sinne des § 1 KSchG wird die gesetzliche Gewährleistungsfrist für bewegliche Sachen gemäß § 933 ABGB auf ein Jahr ab Übergabe verkürzt. Dies gilt nicht bei Arglist oder bei Übernahme einer Garantie für die Beschaffenheit der Ware.

+

(2) Die in Abs. 1 genannte Fristverkürzung gilt nicht für Ansprüche des Käufers wegen Schäden aus der Verletzung des Lebens, des Körpers oder der Gesundheit, bei grob fahrlässigem oder vorsätzlichem Verhalten oder bei Ansprüchen nach dem Produkthaftungsgesetz.

+

(3) Schadenersatzansprüche wegen Mängeln (§ 933a ABGB) verjähren unabhängig von einer verkürzten Gewährleistungsfrist innerhalb der gesetzlichen Frist von drei Jahren ab Kenntnis von Schaden und Schädiger.

+

(4) Gegenüber Verbrauchern gelten uneingeschränkt die gesetzlichen Gewährleistungs- und Verjährungsfristen (§§ 922 ff ABGB, § 9 KSchG).

+ +

§ 10 Rechtswahl und Gerichtsstand

+

(1) Für sämtliche Rechtsverhältnisse zwischen uns und dem Käufer gilt ausschließlich das materielle Recht der Republik Österreich unter Ausschluss des UN-Kaufrechts (CISG) und sonstiger internationaler Kollisionsnormen, soweit zwingende Verbraucherschutzvorschriften nicht entgegenstehen.

+

(2) Ist der Käufer Unternehmer im Sinne des § 1 KSchG, so wird für alle Streitigkeiten aus oder im Zusammenhang mit diesem Vertrag einschließlich seiner Gültigkeit und Durchführung das sachlich zuständige Gericht in Graz vereinbart. Wir sind jedoch berechtigt, auch am allgemeinen Gerichtsstand des Käufers oder an einem sonst gesetzlich zulässigen Gerichtsstand Klage zu erheben.

+

(3) Gegenüber Verbrauchern gelten die gesetzlichen Gerichtsstandregelungen. Eine abweichende Gerichtsstandsvereinbarung mit Verbrauchern wird nicht getroffen.

+ +

§ 11 Schlussbestimmungen

+

Sollten einzelne Bestimmungen dieses Vertrages unwirksam oder nichtig sein oder werden, so berührt dies die Gültigkeit der übrigen Bestimmungen dieses Vertrages nicht. Die Parteien verpflichten sich, unwirksame oder nichtige Bestimmungen durch neue Bestimmungen zu ersetzen, die dem in den unwirksamen oder nichtigen Bestimmungen enthaltenen wirtschaftlichen Regelungsgehalt in rechtlich zulässiger Weise gerecht werden. Entsprechendes gilt, wenn sich in dem Vertrag eine Lücke herausstellen sollte. Zur Ausfüllung der Lücke verpflichten sich die Parteien auf die Etablierung angemessener Regelungen in diesem Vertrag hinzuwirken, die dem am nächsten kommen, was die Vertragsschließenden nach dem Sinn und Zweck dieses Vertrages bestimmt hätten, wenn der Punkt von ihnen bedacht worden wäre.

+ +

Stand der Allgemeinen Geschäftsbedingungen Profit Planet Kaffee-Service: 01.08.2025

+ +
+ +
+ + + + + diff --git a/src/app/admin/contract-management/components/companySettingsPanel.tsx b/src/app/admin/contract-management/components/companySettingsPanel.tsx index fa6e628..22943b7 100644 --- a/src/app/admin/contract-management/components/companySettingsPanel.tsx +++ b/src/app/admin/contract-management/components/companySettingsPanel.tsx @@ -3,6 +3,33 @@ import { useState, useEffect } from 'react' import useContractManagement from '../hooks/useContractManagement' +function fileToDataUrl(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onerror = () => reject(new Error('Failed to read file')) + reader.onload = () => { + const result = reader.result + if (typeof result === 'string') resolve(result) + else reject(new Error('Invalid file result')) + } + reader.readAsDataURL(file) + }) +} + +function summarizeForLog(payload: Record) { + const out: Record = {} + for (const [k, v] of Object.entries(payload)) { + if (typeof v === 'string' && (k.toLowerCase().includes('base64') || k.toLowerCase().includes('qr_code'))) { + out[k] = { kind: 'base64', len: v.length, head: v.slice(0, 32) } + } else if (typeof v === 'string' && v.length > 200) { + out[k] = { kind: 'string', len: v.length, head: v.slice(0, 32) } + } else { + out[k] = v + } + } + return out +} + export default function CompanySettingsPanel() { const { getCompanySettings, updateCompanySettings } = useContractManagement() @@ -12,9 +39,14 @@ export default function CompanySettingsPanel() { company_postal_city: '', company_country: '', }) + const [hasQr60, setHasQr60] = useState(false) + const [hasQr120, setHasQr120] = useState(false) + const [qr60DataUrl, setQr60DataUrl] = useState('') + const [qr120DataUrl, setQr120DataUrl] = useState('') const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [saved, setSaved] = useState(false) + const [saveError, setSaveError] = useState('') useEffect(() => { getCompanySettings() @@ -25,6 +57,11 @@ export default function CompanySettingsPanel() { company_postal_city: data.company_postal_city || '', company_country: data.company_country || '', }) + + const qr60 = (data as any)?.qr_code_60_base64 ?? (data as any)?.qrCode60Base64 + const qr120 = (data as any)?.qr_code_120_base64 ?? (data as any)?.qrCode120Base64 + setHasQr60(!!qr60) + setHasQr120(!!qr120) }) .catch(() => {}) .finally(() => setLoading(false)) @@ -40,17 +77,81 @@ export default function CompanySettingsPanel() { e.preventDefault() setSaving(true) setSaved(false) + setSaveError('') try { - await updateCompanySettings(form) + // IMPORTANT: send `payload` (full strings), not the redacted log view. + const payload: any = { ...form } + if (qr60DataUrl) payload.qr_code_60_base64 = qr60DataUrl + if (qr120DataUrl) payload.qr_code_120_base64 = qr120DataUrl + + // For logging only (redacted); never send this object. + const logPayload: any = summarizeForLog(payload) + + try { + const qr60 = payload.qr_code_60_base64 + const qr120 = payload.qr_code_120_base64 + console.info('[CompanySettingsPanel] updateCompanySettings payload', { + logPayload, + keys: Object.keys(payload), + jsonLength: JSON.stringify(payload).length, + qrFieldTypes: { + qr_code_60_base64: qr60 ? typeof qr60 : null, + qr_code_120_base64: qr120 ? typeof qr120 : null, + }, + qrFieldLengths: { + qr_code_60_base64: typeof qr60 === 'string' ? qr60.length : null, + qr_code_120_base64: typeof qr120 === 'string' ? qr120.length : null, + }, + }) + + if (qr60 && typeof qr60 !== 'string') console.warn('[CompanySettingsPanel] qr_code_60_base64 is not a string!', qr60) + if (qr120 && typeof qr120 !== 'string') console.warn('[CompanySettingsPanel] qr_code_120_base64 is not a string!', qr120) + } catch {} + + await updateCompanySettings(payload) setSaved(true) setTimeout(() => setSaved(false), 3000) } catch { - // silent + setSaveError('Could not save settings.') } finally { setSaving(false) } } + const handleQrUpload = async (which: '60' | '120', file: File | null) => { + setSaved(false) + setSaveError('') + if (!file) return + + // Backend accepts 10MB JSON, but base64 expands the payload. + // Keep a conservative limit to avoid 413 Payload Too Large. + const MAX_FILE_BYTES = 7_000_000 + if (file.size > MAX_FILE_BYTES) { + setSaveError('QR image is too large. Please upload a smaller PNG.') + return + } + if (file.type && file.type !== 'image/png') { + setSaveError('Please upload a PNG file for the QR code.') + return + } + + try { + const dataUrl = await fileToDataUrl(file) + // Normalize to raw base64, to match other endpoints (e.g. company stamp upload) + const m = dataUrl.match(/^data:(.+?);base64,(.*)$/) + const base64 = m ? m[2] : dataUrl + if (which === '60') { + setQr60DataUrl(base64) + setHasQr60(true) + } else { + setQr120DataUrl(base64) + setHasQr120(true) + } + } catch { + setSaveError('Could not read QR image file.') + } + } + if (loading) { return (
@@ -121,6 +222,38 @@ export default function CompanySettingsPanel() {
+
+
+ + handleQrUpload('60', e.target.files?.[0] || null)} + className="block w-full text-sm text-gray-700 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-900 hover:file:bg-blue-100" + /> +
+ {qr60DataUrl ? 'Selected (will be saved on Save)' : hasQr60 ? 'Already uploaded' : 'Not uploaded'} +
+
+ +
+ + handleQrUpload('120', e.target.files?.[0] || null)} + className="block w-full text-sm text-gray-700 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-900 hover:file:bg-blue-100" + /> +
+ {qr120DataUrl ? 'Selected (will be saved on Save)' : hasQr120 ? 'Already uploaded' : 'Not uploaded'} +
+
+
+ + {saveError && ( +
{saveError}
+ )} +
+ +
+ + + {error && ( +
+ {error} +
+ )} + + {savedAt && ( +
+ Saved at {new Date(savedAt).toLocaleTimeString('de-DE')} +
+ )} + + {hasValidationErrors && ( +
+ Please ensure every platform has a title and a valid link (must start with “/” or “http(s)://”). +
+ )} + +
+ {loading && ( +
+ Loading… +
+ )} + + {!loading && platforms.map(platform => ( +
+
+
+
+
{platform.title}
+
{platform.href}
+
+
+ + + +
+
+ + {isOpen(platform) && ( +
+ + + + + + + + + + +
+ + + +
+ + {platform.disabled && ( + + )} +
+ )} +
+
+ ))} + + {!loading && platforms.length === 0 && ( +
+ No platforms configured. +
+ )} +
+ + + + + ) +} diff --git a/src/app/admin/finance-management/page.tsx b/src/app/admin/finance-management/page.tsx index 9d6f73d..96d72ae 100644 --- a/src/app/admin/finance-management/page.tsx +++ b/src/app/admin/finance-management/page.tsx @@ -278,6 +278,7 @@ export default function FinanceManagementPage() { Invoice Customer Issued + Due Date Amount Status Actions @@ -286,12 +287,12 @@ export default function FinanceManagementPage() { {invLoading ? ( <> -
-
+
+
) : filteredBills.length === 0 ? ( - + Keine Rechnungen gefunden. @@ -301,6 +302,24 @@ export default function FinanceManagementPage() { {inv.invoice_number ?? inv.id} {inv.buyer_name ?? '—'} {inv.issued_at ? new Date(inv.issued_at).toLocaleDateString() : '—'} + + {(() => { + if (!inv.due_at) return + const due = new Date(inv.due_at) + const now = new Date() + const diffDays = Math.ceil((due.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) + let cls = 'bg-green-100 text-green-700' // plenty of time + if (inv.status === 'paid') cls = 'bg-green-100 text-green-700' + else if (diffDays < 0) cls = 'bg-red-100 text-red-700' + else if (diffDays <= 3) cls = 'bg-red-100 text-red-700' + else if (diffDays <= 7) cls = 'bg-amber-100 text-amber-700' + return ( + + {due.toLocaleDateString()} + + ) + })()} + €{Number(inv.total_gross ?? 0).toFixed(2)}{' '} {inv.currency ?? 'EUR'} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index b085e30..639f8c2 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -279,6 +279,24 @@ export default function AdminDashboardPage() { + {/* Dashboard Management */} + + {/* User Management (unchanged) */} +
+ + {shippingFeesError && ( +
{shippingFeesError}
+ )} + +
+ {([60, 120] as CoffeeShippingFeePieceCount[]).map((pieceCount) => { + const saving = shippingFeeSaving[pieceCount]; + const savedAt = shippingFeeSavedAt[pieceCount]; + const fieldError = shippingFeeFieldError[pieceCount]; + const current = shippingFees.find((r) => r.pieceCount === pieceCount); + const draft = shippingFeeDraft[pieceCount] ?? ''; + + return ( +
+
+
+
+
{pieceCount} pieces
+ {typeof current?.price === 'number' && Number.isFinite(current.price) ? ( +
Current: €{formatPriceDraft(current.price)}
+ ) : null} + {savedAt ? ( +
+ Saved +
+ ) : null} +
+ {fieldError ? ( +
{fieldError}
+ ) : ( +
Enter a price in EUR (≥ 0).
+ )} +
+ +
+
+ + { + const v = e.target.value; + setShippingFeeDraft((prev) => ({ ...prev, [pieceCount]: v })); + setShippingFeeFieldError((prev) => ({ ...prev, [pieceCount]: null })); + }} + placeholder="0.00" + /> +
+ +
+
+
+ ); + })} +
+ +
{loading && (
Loading…
@@ -131,7 +315,7 @@ export default function AdminSubscriptionsPage() {

Delete coffee?

-

You are about to delete the coffee "{deleteTarget.title}". This action cannot be undone.

+

You are about to delete the coffee “{deleteTarget.title}”. This action cannot be undone.

+ + {shippingError && ( +
+ Shipping fees could not be loaded: {shippingError} +
+ )}

2. Choose coffees & quantities

@@ -298,10 +347,25 @@ export default function CoffeeAbonnementPage() {
))} + + {/* Shipping */} +
+ Shipping + + {shippingLoading ? ( + 'Loading…' + ) : isFreeShippingSelected ? ( + 'FREE SHIPPING' + ) : ( + `€${selectedShippingFee.toFixed(2)}` + )} + +
+
Total (net) - €{totalPrice.toFixed(2)} + €{totalNetWithShipping.toFixed(2)}
diff --git a/src/app/coffee-abonnements/summary/components/SignaturePad.tsx b/src/app/coffee-abonnements/summary/components/SignaturePad.tsx new file mode 100644 index 0000000..c81c7d3 --- /dev/null +++ b/src/app/coffee-abonnements/summary/components/SignaturePad.tsx @@ -0,0 +1,167 @@ +'use client' + +import React, { useEffect, useRef } from 'react' + +type Props = { + value: string + onChange: (dataUrl: string) => void + className?: string +} + +export default function SignaturePad({ value, onChange, className }: Props) { + const canvasRef = useRef(null) + const isDrawing = useRef(false) + + const getPos = (e: React.MouseEvent | React.TouchEvent) => { + const canvas = canvasRef.current + if (!canvas) return { x: 0, y: 0 } + const rect = canvas.getBoundingClientRect() + + const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX + const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY + + return { x: clientX - rect.left, y: clientY - rect.top } + } + + const setupCanvas = () => { + const canvas = canvasRef.current + if (!canvas) return + const rect = canvas.getBoundingClientRect() + const dpr = window.devicePixelRatio || 1 + canvas.width = rect.width * dpr + canvas.height = rect.height * dpr + + const ctx = canvas.getContext('2d') + if (!ctx) return + + ctx.setTransform(dpr, 0, 0, dpr, 0, 0) + ctx.lineWidth = 2 + ctx.lineCap = 'round' + ctx.strokeStyle = '#1C2B4A' + + // If we already have a signature value, redraw it after resize. + if (value) { + const img = new Image() + img.onload = () => { + try { + ctx.clearRect(0, 0, rect.width, rect.height) + ctx.drawImage(img, 0, 0, rect.width, rect.height) + } catch {} + } + img.src = value + } + } + + useEffect(() => { + setupCanvas() + + const onResize = () => setupCanvas() + window.addEventListener('resize', onResize) + return () => window.removeEventListener('resize', onResize) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + // If parent sets a new value (e.g., cleared externally), reflect it. + useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + const ctx = canvas.getContext('2d') + if (!ctx) return + + const rect = canvas.getBoundingClientRect() + ctx.clearRect(0, 0, rect.width, rect.height) + + if (!value) return + + const img = new Image() + img.onload = () => { + try { + ctx.drawImage(img, 0, 0, rect.width, rect.height) + } catch {} + } + img.src = value + }, [value]) + + const startDrawing = (e: React.MouseEvent | React.TouchEvent) => { + e.preventDefault() + const canvas = canvasRef.current + if (!canvas) return + const ctx = canvas.getContext('2d') + if (!ctx) return + + const { x, y } = getPos(e) + ctx.beginPath() + ctx.moveTo(x, y) + isDrawing.current = true + } + + const draw = (e: React.MouseEvent | React.TouchEvent) => { + e.preventDefault() + if (!isDrawing.current) return + + const canvas = canvasRef.current + if (!canvas) return + const ctx = canvas.getContext('2d') + if (!ctx) return + + const { x, y } = getPos(e) + ctx.lineTo(x, y) + ctx.stroke() + } + + const endDrawing = () => { + if (!isDrawing.current) return + isDrawing.current = false + + const canvas = canvasRef.current + if (!canvas) return + + try { + onChange(canvas.toDataURL('image/png')) + } catch { + onChange('') + } + } + + const clear = () => { + const canvas = canvasRef.current + if (!canvas) return + const ctx = canvas.getContext('2d') + if (!ctx) return + + const rect = canvas.getBoundingClientRect() + ctx.clearRect(0, 0, rect.width, rect.height) + onChange('') + } + + return ( +
+
+

Signature

+ +
+
+ +
+

+ {value ? 'Signature captured.' : 'Draw your signature in the box.'} +

+
+ ) +} diff --git a/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts b/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts index 203f44e..1b6637b 100644 --- a/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts +++ b/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts @@ -11,8 +11,7 @@ export type SubscribeAboInput = { target_user_id?: number recipient_name?: string recipient_email?: string - recipient_notes?: string - // NEW: customer fields + // Customer fields firstName?: string lastName?: string email?: string @@ -21,8 +20,22 @@ export type SubscribeAboInput = { city?: string country?: string frequency?: string - startDate?: string - // NEW: logged-in user id + // New contract / contact fields + phone?: string + recipientContractName?: string + recipientAddress?: string + paymentMethod?: string + invoiceByEmail?: boolean + invoiceSameAsShipping?: boolean + invoiceFullName?: string + invoiceStreet?: string + invoicePostalCode?: string + invoiceCity?: string + invoicePhone?: string + invoiceEmail?: string + signingCity?: string + signatureDataUrl?: string + // logged-in user id referred_by?: number | string } @@ -48,7 +61,7 @@ export async function subscribeAbo(input: SubscribeAboInput) { } // NEW: validate customer fields (required in UI) - const requiredFields = ['firstName','lastName','email','street','postalCode','city','country','frequency'] as const + const requiredFields = ['firstName','lastName','email','street','postalCode','city','country'] as const const missing = requiredFields.filter(k => { const v = (input as any)[k] return typeof v !== 'string' || v.trim() === '' @@ -62,7 +75,7 @@ export async function subscribeAbo(input: SubscribeAboInput) { interval_count: input.interval_count ?? 1, is_auto_renew: input.is_auto_renew ?? true, is_for_self: isForSelf, - // NEW: include customer fields + // Customer fields firstName: input.firstName, lastName: input.lastName, email: input.email, @@ -71,7 +84,25 @@ export async function subscribeAbo(input: SubscribeAboInput) { city: input.city, country: input.country?.toUpperCase?.() ?? input.country, frequency: input.frequency, - startDate: input.startDate || undefined, + // New contract / contact fields + phone: input.phone || undefined, + recipientContractName: input.recipientContractName || undefined, + recipientAddress: input.recipientAddress || undefined, + paymentMethod: input.paymentMethod || undefined, + invoiceByEmail: input.invoiceByEmail ?? false, + invoiceSameAsShipping: input.invoiceSameAsShipping ?? true, + signingCity: input.signingCity || undefined, + signatureDataUrl: input.signatureDataUrl || undefined, + } + + // Include invoice address fields when not same as shipping + if (!body.invoiceSameAsShipping) { + body.invoiceFullName = input.invoiceFullName || undefined + body.invoiceStreet = input.invoiceStreet || undefined + body.invoicePostalCode = input.invoicePostalCode || undefined + body.invoiceCity = input.invoiceCity || undefined + body.invoicePhone = input.invoicePhone || undefined + body.invoiceEmail = input.invoiceEmail || undefined } if (hasItems) { body.items = input.items!.map(i => ({ @@ -92,7 +123,6 @@ export async function subscribeAbo(input: SubscribeAboInput) { if (input.target_user_id != null) body.target_user_id = input.target_user_id if (!isForSelf && input.recipient_email) body.recipient_email = input.recipient_email if (!isForSelf && input.recipient_name) body.recipient_name = input.recipient_name - if (!isForSelf && input.recipient_notes) body.recipient_notes = input.recipient_notes // NEW: always include referred_by if provided if (input.referred_by != null) body.referred_by = input.referred_by diff --git a/src/app/coffee-abonnements/summary/hooks/useAboActiveContractHtml.ts b/src/app/coffee-abonnements/summary/hooks/useAboActiveContractHtml.ts new file mode 100644 index 0000000..cb9cf38 --- /dev/null +++ b/src/app/coffee-abonnements/summary/hooks/useAboActiveContractHtml.ts @@ -0,0 +1,105 @@ +'use client' + +import { useEffect, useState } from 'react' +import { authFetch } from '../../../utils/authFetch' + +const apiBase = (process.env.NEXT_PUBLIC_API_BASE_URL || '').replace(/\/+$/, '') + +export function useAboActiveContractHtml() { + const [html, setHtml] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + let active = true + + ;(async () => { + setLoading(true) + setError(null) + try { + const url = `${apiBase}/api/contracts/abo/active` + const res = await authFetch(url, { + method: 'GET', + headers: { Accept: 'application/json' }, + credentials: 'include', + }) + + const ct = res.headers.get('content-type') || '' + const isJson = ct.includes('application/json') + + try { + console.info('[useAboActiveContractHtml] response meta', { + url, + status: res.status, + ok: res.ok, + contentType: ct, + isJson, + }) + } catch {} + + if (isJson) { + const payload: any = await res.json().catch(() => null) + + try { + console.info('[useAboActiveContractHtml] response json keys', payload && typeof payload === 'object' ? Object.keys(payload) : payload) + } catch {} + + if (!res.ok) { + const msg = payload?.message || payload?.error || `Failed to load contract: ${res.status}` + throw new Error(msg) + } + + const foundRaw = payload?.found ?? payload?.data?.found + const found = typeof foundRaw === 'boolean' ? foundRaw : undefined + + const htmlRaw = payload?.html ?? payload?.data?.html + const htmlValue = typeof htmlRaw === 'string' ? htmlRaw : '' + + const isFound = (typeof found === 'boolean') ? found : Boolean(htmlValue) + + try { + console.info('[useAboActiveContractHtml] parsed json', { + success: payload?.success, + found: isFound, + htmlLength: htmlValue ? htmlValue.length : 0, + htmlPreview: htmlValue ? `${htmlValue.slice(0, 200)}${htmlValue.length > 200 ? '…' : ''}` : '', + }) + } catch {} + + if (active) setHtml(isFound && htmlValue ? htmlValue : null) + return + } + + // Fallback: older endpoints returned raw HTML. + const text = await res.text().catch(() => '') + + try { + console.info('[useAboActiveContractHtml] response text preview', { + status: res.status, + ok: res.ok, + textLength: text.length, + textPreview: text ? `${text.slice(0, 200)}${text.length > 200 ? '…' : ''}` : '', + }) + } catch {} + + if (!res.ok) { + throw new Error(text || `Failed to load contract: ${res.status}`) + } + if (active) setHtml(text || null) + } catch (e: any) { + if (active) { + setHtml(null) + setError(e?.message || 'Failed to load contract preview.') + } + } finally { + if (active) setLoading(false) + } + })() + + return () => { + active = false + } + }, []) + + return { html, loading, error } +} diff --git a/src/app/coffee-abonnements/summary/hooks/useAboContractTemplateHtml.ts b/src/app/coffee-abonnements/summary/hooks/useAboContractTemplateHtml.ts new file mode 100644 index 0000000..3c2d842 --- /dev/null +++ b/src/app/coffee-abonnements/summary/hooks/useAboContractTemplateHtml.ts @@ -0,0 +1,49 @@ +'use client' + +import { useEffect, useState } from 'react' +import { authFetch } from '../../../utils/authFetch' + +const apiBase = (process.env.NEXT_PUBLIC_API_BASE_URL || '').replace(/\/+$/, '') + +export function useAboContractTemplateHtml() { + const [html, setHtml] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + let active = true + + ;(async () => { + setLoading(true) + setError(null) + try { + const res = await authFetch(`${apiBase}/api/contracts/abo/active`, { + method: 'GET', + headers: { Accept: 'text/html' }, + credentials: 'include', + }) + + const text = await res.text().catch(() => '') + + if (!res.ok) { + throw new Error(text || `Failed to load contract template: ${res.status}`) + } + + if (active) setHtml(text || null) + } catch (e: any) { + if (active) { + setHtml(null) + setError(e?.message || 'Failed to load contract template preview.') + } + } finally { + if (active) setLoading(false) + } + })() + + return () => { + active = false + } + }, []) + + return { html, loading, error } +} diff --git a/src/app/coffee-abonnements/summary/page.tsx b/src/app/coffee-abonnements/summary/page.tsx index b3b549d..5ed3dad 100644 --- a/src/app/coffee-abonnements/summary/page.tsx +++ b/src/app/coffee-abonnements/summary/page.tsx @@ -1,19 +1,62 @@ 'use client'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import PageLayout from '../../components/PageLayout'; import { useRouter } from 'next/navigation'; import { useActiveCoffees } from '../hooks/getActiveCoffees'; import { getStandardVatRate, getVatRates } from './hooks/getTaxRate'; import { subscribeAbo } from './hooks/subscribeAbo'; import useAuthStore from '../../store/authStore' +import { useShippingFees } from '../hooks/useShippingFees'; +import { useAboContractTemplateHtml } from './hooks/useAboContractTemplateHtml' +import SignaturePad from './components/SignaturePad' +import { Dialog, DialogActions, DialogBody, DialogTitle } from '../../components/dialog' +import { createReferralLink } from '../../referral-management/hooks/generateReferralLink' + +const COLORS = ['#1C2B4A', '#233357', '#2A3B66', '#314475', '#3A4F88', '#5B6C9A']; // dark blue palette + +function extractTemplateVariables(templateHtml: string | null | undefined): string[] { + if (!templateHtml) return [] + const vars = new Set() + const re = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g + let match: RegExpExecArray | null + while ((match = re.exec(templateHtml)) !== null) { + if (match[1]) vars.add(match[1]) + } + return Array.from(vars).sort((a, b) => a.localeCompare(b)) +} + +function escapeHtml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +function hashString(value: string): number { + // djb2 + let hash = 5381 + for (let i = 0; i < value.length; i++) { + hash = ((hash << 5) + hash) ^ value.charCodeAt(i) + } + return hash >>> 0 +} export default function SummaryPage() { const router = useRouter(); const { coffees, loading, error } = useActiveCoffees(); const user = useAuthStore(state => state.user) + const { feeByPieceCount, loading: shippingLoading, error: shippingError } = useShippingFees(); + const { html: contractHtml, loading: contractLoading, error: contractError } = useAboContractTemplateHtml() + const [isContractPreviewOpen, setIsContractPreviewOpen] = useState(false) + const [contractPdfUrl, setContractPdfUrl] = useState('') + const [contractPdfKey, setContractPdfKey] = useState('') + const [contractPdfLoading, setContractPdfLoading] = useState(false) + const [contractPdfError, setContractPdfError] = useState(null) const [selections, setSelections] = useState>({}); const [selectedPlanCapsules, setSelectedPlanCapsules] = useState<60 | 120>(120); - const [isForSelf, setIsForSelf] = useState(true); + const [signatureDataUrl, setSignatureDataUrl] = useState('') const [form, setForm] = useState({ firstName: '', lastName: '', @@ -22,19 +65,284 @@ export default function SummaryPage() { postalCode: '', city: '', country: 'DE', - frequency: 'monatlich', - startDate: '', - recipientEmail: '', - recipientName: '', - recipientNotes: '', + phone: '', + paymentMethod: 'sepa' as 'sepa' | 'card' | 'sofort', + invoiceByEmail: true, + invoiceSameAsShipping: true, + invoiceFullName: '', + invoiceStreet: '', + invoicePostalCode: '', + invoiceCity: '', + invoicePhone: '', + invoiceEmail: '', + signingCity: '', }); const [showThanks, setShowThanks] = useState(false); + const [guestMailtoHref, setGuestMailtoHref] = useState('') + const [guestInviteLink, setGuestInviteLink] = useState('') const [confetti, setConfetti] = useState<{ left: number; delay: number; color: string }[]>([]); const [taxRate, setTaxRate] = useState(0.07); // minimal fallback only const [vatRates, setVatRates] = useState<{ code: string; rate: number | null }[]>([]); const [submitError, setSubmitError] = useState(null); const [submitLoading, setSubmitLoading] = useState(false); - const COLORS = ['#1C2B4A', '#233357', '#2A3B66', '#314475', '#3A4F88', '#5B6C9A']; // dark blue palette + const initialCountryRef = useRef(form.country) + + const templateVariableNames = useMemo(() => extractTemplateVariables(contractHtml), [contractHtml]) + const templateVariableNamesKey = useMemo(() => templateVariableNames.join('|'), [templateVariableNames]) + const [contractVariables, setContractVariables] = useState>({}) + + // Auto-compute contract variables from form state for preview + useEffect(() => { + if (!templateVariableNamesKey) return + const fullName = `${form.firstName} ${form.lastName}`.trim() + const isCompany = user?.userType === 'company' || user?.user_type === 'company' + const invoiceSame = form.invoiceSameAsShipping + + const computed: Record = { + contractNumber: '(wird generiert)', + currentDate: new Date().toLocaleDateString('de-AT', { day: '2-digit', month: '2-digit', year: 'numeric' }), + recipientName: fullName, + recipientAddress: `${form.street}, ${form.postalCode} ${form.city}`.trim(), + shippingCustomerClass: isCompany ? '' : 'checked', + shippingCompanyClass: isCompany ? 'checked' : '', + shippingFullName: fullName, + shippingStreet: form.street, + shippingPostalCode: form.postalCode, + shippingCity: form.city, + shippingPhone: form.phone, + shippingEmail: form.email, + invoiceSameAsShippingMark: invoiceSame ? '✓' : '', + invoiceCompanyClass: isCompany ? 'checked' : '', + invoiceCustomerClass: isCompany ? '' : 'checked', + invoiceFullName: invoiceSame ? fullName : form.invoiceFullName, + invoiceStreet: invoiceSame ? form.street : form.invoiceStreet, + invoicePostalCode: invoiceSame ? form.postalCode : form.invoicePostalCode, + invoiceCity: invoiceSame ? form.city : form.invoiceCity, + invoicePhone: invoiceSame ? form.phone : form.invoicePhone, + invoiceEmail: invoiceSame ? form.email : form.invoiceEmail, + fnCheckedClass: '', + fnNumber: '', + atuCheckedClass: '', + atuNumber: '', + entrepreneurClass: isCompany ? 'checked' : '', + consumerClass: isCompany ? '' : 'checked', + paymentSepaClass: form.paymentMethod === 'sepa' ? 'checked' : '', + paymentCardClass: form.paymentMethod === 'card' ? 'checked' : '', + paymentSofortClass: form.paymentMethod === 'sofort' ? 'checked' : '', + invoiceByEmailClass: form.invoiceByEmail ? 'checked' : '', + signingCity: form.signingCity, + fullName, + } + setContractVariables(computed) + }, [templateVariableNamesKey, form, user, signatureDataUrl]) + + const populatedContractHtml = useMemo(() => { + if (!contractHtml) return null + // Replace placeholders with escaped user-entered values so placeholders never show. + return contractHtml.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (_whole, varName: string) => { + if (varName === 'signatureImage' && !contractVariables[varName] && signatureDataUrl) { + const safeUrl = String(signatureDataUrl) + if (safeUrl.startsWith('data:image/')) { + const src = escapeHtml(safeUrl) + return `Signature` + } + } + + const value = contractVariables[varName] ?? '' + return escapeHtml(String(value)) + }) + }, [contractHtml, contractVariables, signatureDataUrl]) + + const contractPdfCacheKey = useMemo(() => { + if (!populatedContractHtml) return '' + return String(hashString(populatedContractHtml)) + }, [populatedContractHtml]) + + useEffect(() => { + // Cleanup blob URL when it changes or on unmount. + return () => { + if (contractPdfUrl) URL.revokeObjectURL(contractPdfUrl) + } + }, [contractPdfUrl]) + + const closeContractPreview = () => { + setIsContractPreviewOpen(false) + setContractPdfError(null) + setContractPdfLoading(false) + setContractPdfKey('') + if (contractPdfUrl) { + URL.revokeObjectURL(contractPdfUrl) + setContractPdfUrl('') + } + } + + const openContractPreview = async () => { + if (!populatedContractHtml) return + + setIsContractPreviewOpen(true) + setContractPdfError(null) + + // Reuse only if the populated HTML hasn't changed. + if (contractPdfUrl && contractPdfKey === contractPdfCacheKey) return + + if (contractPdfUrl) { + URL.revokeObjectURL(contractPdfUrl) + setContractPdfUrl('') + } + + setContractPdfLoading(true) + try { + const [jsPdfMod, html2canvasMod] = await Promise.all([import('jspdf'), import('html2canvas')]) + const jsPDF: any = (jsPdfMod as any).jsPDF || (jsPdfMod as any).default + const html2canvas: any = (html2canvasMod as any).default || html2canvasMod + + const parser = new DOMParser() + const doc = parser.parseFromString(populatedContractHtml, 'text/html') + const styles = Array.from(doc.querySelectorAll('style')) + .map(s => s.textContent || '') + .join('\n') + const bodyHtml = doc.body?.innerHTML || '' + + const wrapper = document.createElement('div') + wrapper.style.position = 'fixed' + wrapper.style.left = '-10000px' + wrapper.style.top = '0' + wrapper.style.width = '794px' // approx A4 width at 96dpi + wrapper.style.background = '#ffffff' + wrapper.innerHTML = `${bodyHtml}` + document.body.appendChild(wrapper) + + try { + const pdf = new jsPDF({ orientation: 'p', unit: 'pt', format: 'a4' }) + + const pageWidth = pdf.internal.pageSize.getWidth() + const pageHeight = pdf.internal.pageSize.getHeight() + + const marginX = 24 + const marginTop = 24 + const marginBottom = 24 + + const usableWidth = pageWidth - marginX * 2 + const usableHeight = pageHeight - marginTop - marginBottom + + const renderCanvasToPdf = (canvas: HTMLCanvasElement, pageIndex: number) => { + const imgWidthPt = usableWidth + const pxPerPt = canvas.width / imgWidthPt + const sliceHeightPx = Math.max(1, Math.floor(usableHeight * pxPerPt)) + + let yPx = 0 + let sliceIndex = 0 + while (yPx < canvas.height) { + const remainingPx = canvas.height - yPx + const currentSliceHeightPx = Math.min(sliceHeightPx, remainingPx) + + const sliceCanvas = document.createElement('canvas') + sliceCanvas.width = canvas.width + sliceCanvas.height = currentSliceHeightPx + const ctx = sliceCanvas.getContext('2d') + if (!ctx) break + + ctx.drawImage( + canvas, + 0, + yPx, + canvas.width, + currentSliceHeightPx, + 0, + 0, + canvas.width, + currentSliceHeightPx + ) + + const imgData = sliceCanvas.toDataURL('image/png') + const imgHeightPt = currentSliceHeightPx / pxPerPt + + const isFirstOverall = pageIndex === 0 && sliceIndex === 0 + if (!isFirstOverall) pdf.addPage() + pdf.addImage(imgData, 'PNG', marginX, marginTop, imgWidthPt, imgHeightPt) + + yPx += currentSliceHeightPx + sliceIndex++ + } + } + + // Split at explicit .pageBreak markers (in document order, even when nested) + // to avoid cutting content between pages. + const docRoot = wrapper.querySelector('.doc') as HTMLElement | null + const pageRoot = docRoot ?? wrapper + const breakEls = Array.from(pageRoot.querySelectorAll('.pageBreak')) as HTMLElement[] + + if (breakEls.length === 0) { + const canvas: HTMLCanvasElement = await html2canvas(wrapper, { + scale: Math.min(2, window.devicePixelRatio || 1), + backgroundColor: '#ffffff', + useCORS: true, + }) + renderCanvasToPdf(canvas, 0) + } else { + const range = document.createRange() + range.setStart(pageRoot, 0) + + const fragments: DocumentFragment[] = [] + for (const br of breakEls) { + range.setEndBefore(br) + const frag = range.cloneContents() + if (frag.childNodes.length > 0) fragments.push(frag) + range.setStartAfter(br) + } + range.setEnd(pageRoot, pageRoot.childNodes.length) + const lastFrag = range.cloneContents() + if (lastFrag.childNodes.length > 0) fragments.push(lastFrag) + + if (fragments.length === 0) { + const canvas: HTMLCanvasElement = await html2canvas(wrapper, { + scale: Math.min(2, window.devicePixelRatio || 1), + backgroundColor: '#ffffff', + useCORS: true, + }) + renderCanvasToPdf(canvas, 0) + } else { + for (let pageIndex = 0; pageIndex < fragments.length; pageIndex++) { + const pageWrapper = document.createElement('div') + pageWrapper.style.position = 'fixed' + pageWrapper.style.left = '-10000px' + pageWrapper.style.top = '0' + pageWrapper.style.width = '794px' + pageWrapper.style.background = '#ffffff' + pageWrapper.innerHTML = `` + + const pageDoc = document.createElement('div') + pageDoc.className = docRoot?.className || 'doc' + pageDoc.appendChild(fragments[pageIndex]) + pageWrapper.appendChild(pageDoc) + document.body.appendChild(pageWrapper) + try { + const canvas: HTMLCanvasElement = await html2canvas(pageWrapper, { + scale: Math.min(2, window.devicePixelRatio || 1), + backgroundColor: '#ffffff', + useCORS: true, + }) + renderCanvasToPdf(canvas, pageIndex) + } finally { + document.body.removeChild(pageWrapper) + } + } + } + } + + const blob = pdf.output('blob') as Blob + const url = URL.createObjectURL(blob) + setContractPdfUrl(url) + setContractPdfKey(contractPdfCacheKey) + } finally { + document.body.removeChild(wrapper) + } + } catch (e: any) { + setContractPdfError(e?.message || 'Failed to generate PDF preview.') + } finally { + setContractPdfLoading(false) + } + } useEffect(() => { try { @@ -96,18 +404,19 @@ export default function SummaryPage() { useEffect(() => { let active = true; (async () => { - console.info('[SummaryPage] Loading vat rates (mount). country:', form.country) + const mountCountry = initialCountryRef.current + console.info('[SummaryPage] Loading vat rates (mount). country:', mountCountry) const list = await getVatRates(); if (!active) return; console.info('[SummaryPage] getVatRates result count:', list.length) setVatRates(list); - const upper = form.country.toUpperCase(); + const upper = mountCountry.toUpperCase(); const match = list.find(r => r.code === upper); if (match?.rate != null) { console.info('[SummaryPage] Initial taxRate from list:', match.rate, 'country:', upper) setTaxRate(match.rate); } else { - const rate = await getStandardVatRate(form.country); + const rate = await getStandardVatRate(mountCountry); console.info('[SummaryPage] Fallback taxRate via getStandardVatRate:', rate, 'country:', upper) setTaxRate(rate ?? 0.07); } @@ -138,17 +447,29 @@ export default function SummaryPage() { () => selectedEntries.reduce((sum, e) => sum + (e.quantity / 10) * e.coffee.pricePer10, 0), [selectedEntries] ); - const taxAmount = useMemo(() => totalPrice * taxRate, [totalPrice, taxRate]); - const totalWithTax = useMemo(() => totalPrice + taxAmount, [totalPrice, taxRate, taxAmount]); - const handleInput = (e: React.ChangeEvent) => { + const shippingFee = useMemo(() => { + const v = feeByPieceCount[selectedPlanCapsules]; + return Number.isFinite(Number(v)) ? Number(v) : 0; + }, [feeByPieceCount, selectedPlanCapsules]); + + const netWithShipping = useMemo( + () => totalPrice + shippingFee, + [totalPrice, shippingFee] + ); + + const taxAmount = useMemo(() => totalPrice * taxRate, [totalPrice, taxRate]); + const taxAmountWithShipping = useMemo(() => netWithShipping * taxRate, [netWithShipping, taxRate]); + const totalWithTax = useMemo(() => netWithShipping + taxAmountWithShipping, [netWithShipping, taxAmountWithShipping]); + + const handleInput = (e: React.ChangeEvent) => { const { name, value } = e.target; setForm(prev => ({ ...prev, [name]: value })); }; - const handleRecipientNotes = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setForm(prev => ({ ...prev, [name]: value })); + const handleCheckbox = (e: React.ChangeEvent) => { + const { name, checked } = e.target; + setForm(prev => ({ ...prev, [name]: checked })); }; const fillFromLoggedInData = () => { @@ -177,7 +498,7 @@ export default function SummaryPage() { })); }; - const requiredSelfFields: Array = [ + const requiredSelfFields = [ 'firstName', 'lastName', 'email', @@ -185,17 +506,16 @@ export default function SummaryPage() { 'postalCode', 'city', 'country', - 'frequency', - ] + ] as const - const hasRequiredSelfFields = requiredSelfFields.every(k => form[k].trim() !== '') - const hasRequiredGiftFields = isForSelf || form.recipientEmail.trim() !== '' + const hasRequiredSelfFields = requiredSelfFields.every(k => String(form[k]).trim() !== '') + const hasRequiredInvoiceFields = form.invoiceSameAsShipping || form.invoiceEmail.trim() !== '' const canSubmit = selectedEntries.length > 0 && totalPacks === requiredPacks && hasRequiredSelfFields && - hasRequiredGiftFields; + hasRequiredInvoiceFields; const backToSelection = () => router.push('/coffee-abonnements'); @@ -206,10 +526,6 @@ export default function SummaryPage() { setSubmitError(`Order must contain exactly ${requiredPacks} packs (${selectedPlanCapsules} capsules).`) return } - if (!isForSelf && !form.recipientEmail.trim()) { - setSubmitError('Recipient email is required when the subscription is for someone else.') - return - } setSubmitError(null) setSubmitLoading(true) @@ -222,8 +538,7 @@ export default function SummaryPage() { billing_interval: 'month', interval_count: 1, is_auto_renew: true, - is_for_self: isForSelf, - // NEW: pass customer fields + is_for_self: true, firstName: form.firstName.trim(), lastName: form.lastName.trim(), email: form.email.trim(), @@ -231,18 +546,32 @@ export default function SummaryPage() { postalCode: form.postalCode.trim(), city: form.city.trim(), country: form.country.trim(), - frequency: form.frequency.trim(), - startDate: form.startDate.trim() || undefined, - recipient_email: isForSelf ? undefined : form.recipientEmail.trim(), - recipient_name: isForSelf ? undefined : (form.recipientName.trim() || undefined), - recipient_notes: isForSelf ? undefined : (form.recipientNotes.trim() || undefined), - // NEW: always include referred_by if available + frequency: 'monatlich', + phone: form.phone.trim() || undefined, + recipientContractName: `${form.firstName} ${form.lastName}`.trim() || undefined, + paymentMethod: form.paymentMethod, + invoiceByEmail: form.invoiceByEmail, + invoiceSameAsShipping: form.invoiceSameAsShipping, + ...(!form.invoiceSameAsShipping ? { + invoiceFullName: form.invoiceFullName.trim() || undefined, + invoiceStreet: form.invoiceStreet.trim() || undefined, + invoicePostalCode: form.invoicePostalCode.trim() || undefined, + invoiceCity: form.invoiceCity.trim() || undefined, + invoicePhone: form.invoicePhone.trim() || undefined, + invoiceEmail: form.invoiceEmail.trim() || undefined, + } : {}), + signingCity: form.signingCity.trim() || undefined, + signatureDataUrl: signatureDataUrl || undefined, referred_by: typeof currentUserId === 'number' ? currentUserId : undefined, } console.info('[SummaryPage] subscribeAbo payload:', payload) // NEW: explicit JSON preview to match request body console.info('[SummaryPage] subscribeAbo payload JSON:', JSON.stringify(payload)) await subscribeAbo(payload) + + setGuestMailtoHref('') + setGuestInviteLink('') + setShowThanks(true); try { sessionStorage.removeItem('coffeeSelections'); } catch {} try { sessionStorage.removeItem('coffeeAboSizeCapsules'); } catch {} @@ -327,7 +656,6 @@ export default function SummaryPage() { > Fill fields with logged in data - {/* "For someone else" is disabled for now — only self-subscriptions */}
{/* inputs translated */}
@@ -363,51 +691,139 @@ export default function SummaryPage() {
- - + +
-
- - +
+ + {/* Payment method */} +
+

Payment method

+
+ {(['sepa', 'card', 'sofort'] as const).map(method => ( + + ))}
- {!isForSelf && ( - <> + +
+ + {/* Invoice address */} +
+

Invoice address

+ + {!form.invoiceSameAsShipping && ( +
- - + +
- - + +
-
- -