diff --git a/jest.config.ts b/jest.config.mjs similarity index 78% rename from jest.config.ts rename to jest.config.mjs index f686f974..af947ff8 100644 --- a/jest.config.ts +++ b/jest.config.mjs @@ -1,4 +1,3 @@ -import type { Config } from 'jest'; import nextJest from 'next/jest.js'; const createJestConfig = nextJest({ @@ -6,7 +5,8 @@ const createJestConfig = nextJest({ dir: './', }); -const config: Config = { +/** @type {import('jest').Config} */ +const config = { coverageProvider: 'v8', testEnvironment: 'jsdom', transform: { @@ -14,6 +14,10 @@ const config: Config = { }, transformIgnorePatterns: ['/node_modules/(?!@t3-oss)'], setupFilesAfterEnv: ['/src/test/test-setup.ts'], + extensionsToTreatAsEsm: ['.ts', '.tsx'], + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + } }; // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async diff --git a/package.json b/package.json index 56745952..3adbbc1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "next-menu", - "version": "0.0.70", + "version": "0.0.71", "private": true, "type": "module", "scripts": { @@ -19,16 +19,16 @@ "lint:fix": "next lint --fix", "preview": "next build && next start", "start": "next start", + "test": "jest", + "test:watch": "jest --watch", "typecheck": "tsc --noEmit", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", - "test": "jest", - "test:watch": "jest --watch", "knip": "knip" }, "dependencies": { - "@clerk/nextjs": "^6.20.2", - "@clerk/themes": "2.2.48", + "@clerk/nextjs": "^6.21.0", + "@clerk/themes": "2.2.49", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -51,7 +51,7 @@ "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-toast": "^1.2.14", "@radix-ui/react-tooltip": "^1.2.7", - "@sentry/nextjs": "9.25.0", + "@sentry/nextjs": "9.26.0", "@stripe/react-stripe-js": "^3.7.0", "@stripe/stripe-js": "^7.3.1", "@t3-oss/env-nextjs": "^0.13.6", @@ -67,7 +67,7 @@ "embla-carousel-react": "^8.6.0", "geist": "^1.4.2", "jotai": "^2.12.4", - "lucide-react": "^0.511.0", + "lucide-react": "^0.513.0", "next": "15.3.2", "next-themes": "^0.4.6", "postgres": "^3.4.5", @@ -89,7 +89,7 @@ "tailwindcss-animate": "^1.0.7", "truncate-middle": "^2.0.1", "vaul": "^1.1.2", - "zod": "3.25.49" + "zod": "3.25.51" }, "devDependencies": { "@chromatic-com/storybook": "3.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 645672ed..fa754529 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,11 +9,11 @@ importers: .: dependencies: '@clerk/nextjs': - specifier: ^6.20.2 - version: 6.20.2(next@15.3.2(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^6.21.0 + version: 6.21.0(next@15.3.2(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@clerk/themes': - specifier: 2.2.48 - version: 2.2.48 + specifier: 2.2.49 + version: 2.2.49 '@dnd-kit/core': specifier: ^6.3.1 version: 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -81,8 +81,8 @@ importers: specifier: ^1.2.7 version: 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@sentry/nextjs': - specifier: 9.25.0 - version: 9.25.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.2(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.98.0(esbuild@0.25.2)) + specifier: 9.26.0 + version: 9.26.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.2(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.98.0(esbuild@0.25.2)) '@stripe/react-stripe-js': specifier: ^3.7.0 version: 3.7.0(@stripe/stripe-js@7.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -91,7 +91,7 @@ importers: version: 7.3.1 '@t3-oss/env-nextjs': specifier: ^0.13.6 - version: 0.13.6(arktype@2.1.20)(typescript@5.8.3)(zod@3.25.49) + version: 0.13.6(arktype@2.1.20)(typescript@5.8.3)(zod@3.25.51) '@tanstack/react-query': specifier: ^5.79.2 version: 5.79.2(react@19.1.0) @@ -129,8 +129,8 @@ importers: specifier: ^2.12.4 version: 2.12.4(@types/react@19.1.6)(react@19.1.0) lucide-react: - specifier: ^0.511.0 - version: 0.511.0(react@19.1.0) + specifier: ^0.513.0 + version: 0.513.0(react@19.1.0) next: specifier: 15.3.2 version: 15.3.2(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -195,8 +195,8 @@ importers: specifier: ^1.1.2 version: 1.1.2(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) zod: - specifier: 3.25.49 - version: 3.25.49 + specifier: 3.25.51 + version: 3.25.51 devDependencies: '@chromatic-com/storybook': specifier: 3.2.6 @@ -964,32 +964,27 @@ packages: peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - '@clerk/backend@1.34.0': - resolution: {integrity: sha512-9rZ8hQJVpX5KX2bEpiuVXfpjhojQCiqCWADJDdCI0PCeKxn58Ep0JPYiIcczg4VKUc3a7jve9vXylykG2XajLQ==} + '@clerk/backend@2.0.0': + resolution: {integrity: sha512-1P4o9L464KVYjkMPaFBKmWo0WpUZsSO+57xjkrKPjW0FOTylYRicKCafoixfRlPMlYWzx7ETmj0ZypmrAZniBA==} engines: {node: '>=18.17.0'} - peerDependencies: - svix: ^1.62.0 - peerDependenciesMeta: - svix: - optional: true - '@clerk/clerk-react@5.31.8': - resolution: {integrity: sha512-GPhOdI7drAaamiKIhzfWiOVe4zw4wUi1sKp6khgUzcjr9hRopdZvzMts0fU+XLHFnYUSX8IPw4c0CDXY1wBKuw==} + '@clerk/clerk-react@5.31.9': + resolution: {integrity: sha512-jP+qygYcChVDKM3pMtChOGNrGV4QAOYQvVyiitzQu5xgyVsFN3AnSdIj0u73lxOLZubfv9cOHjFwc41s31f1pA==} engines: {node: '>=18.17.0'} peerDependencies: react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0 - '@clerk/nextjs@6.20.2': - resolution: {integrity: sha512-rBtAdx2PFxexBDU41GEmEQwSsfbTU7J7OVBKRtmXAXFMYdknGNw41674sFBTaG+wjbTYrhW7wsXcyphEUyVMoQ==} + '@clerk/nextjs@6.21.0': + resolution: {integrity: sha512-TlX0eVSoxdC4aor2ohlIgn2je4HGWz/tnXAIDhtl1COS7N6ZmaPhcwv9essg1wt7Esps18pKXS19vrgbipHP9g==} engines: {node: '>=18.17.0'} peerDependencies: next: ^13.5.7 || ^14.2.25 || ^15.2.3 react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0 - '@clerk/shared@3.9.5': - resolution: {integrity: sha512-KeIug5qV4LnzZD+16SLkJvdONPs2HQ7I1A7jbHYOGB37vQrQrus64Wu5XeNzbWFTN1Z5fAPSGuja8MfT2cBT4A==} + '@clerk/shared@3.9.6': + resolution: {integrity: sha512-zScvDbNKBcGfkD7Db4LCoEbB8qZ/WFwuB77xqRgXiHDa+pBzEPyFB5nQSt1zQfLqOK3POng0GsPBoXEWKb4Ikw==} engines: {node: '>=18.17.0'} peerDependencies: react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 @@ -1000,12 +995,12 @@ packages: react-dom: optional: true - '@clerk/themes@2.2.48': - resolution: {integrity: sha512-vylu+2IShOCBr6OBhIFlXL3ULqIZc4L5A/muB0HEum7BHR0bjq6UWHyNZuPKla0W4QGc3azPrTgi19zI4PlZ6A==} + '@clerk/themes@2.2.49': + resolution: {integrity: sha512-bwiBE/H/LtYpQL13GXjw1XzA8Vi4KwXiDbDRQCGPuLwFz2PtPLr4uFSGfNP8qLGFrM2hs7UeAPS+7CDlVUOaAA==} engines: {node: '>=18.17.0'} - '@clerk/types@4.59.3': - resolution: {integrity: sha512-xwOO/hfABzbFr3f1RaVXHsDDQ0+jYpm84GiaUDxo+mLsYUgD9f2GmGjKkgWybXzvsBsgZlycSwRXkeDD6utFqg==} + '@clerk/types@4.60.0': + resolution: {integrity: sha512-60u/Z3VD0lgepsySUPyFM1MV5cwhMwouN63na5g9+qm3PpaTE2kN4DeW9Nq6t1YB0TFpVEKIb1r8U6EOWPa01A==} engines: {node: '>=18.17.0'} '@cspotcode/source-map-support@0.8.1': @@ -2772,28 +2767,28 @@ packages: '@rushstack/eslint-patch@1.10.5': resolution: {integrity: sha512-kkKUDVlII2DQiKy7UstOR1ErJP8kUKAQ4oa+SQtM0K+lPdmmjj0YnnxBgtTVYH7mUKtbsxeFC9y0AmK7Yb78/A==} - '@sentry-internal/browser-utils@9.25.0': - resolution: {integrity: sha512-pPlIXHcXNKjVsN/hMeh6ujBkDBMKfxFSdPHHshMSj9tRNc5SI1A1pxWK6QaEMAXor74ICYWt/fazJDw9wE2shg==} + '@sentry-internal/browser-utils@9.26.0': + resolution: {integrity: sha512-Ya4YQSzrM6TSuCuO+tUPK+WXFHfndaX73wszCmIu7UZlUHbKTZ5HVWxXxHW9f6KhVIHYzdYQMeA/4F4N7n+rgg==} engines: {node: '>=18'} - '@sentry-internal/feedback@9.25.0': - resolution: {integrity: sha512-myrU1H1IR3EjRPo/66+Jjy5xHq9xEuosI8iRKN/0dSMeS6TZQ+PF0ixNHlwtyxhJn3z0o1gobB1Oawi7W/EDeQ==} + '@sentry-internal/feedback@9.26.0': + resolution: {integrity: sha512-XnN6UiFNGkJMCw8Oy9qnP2GW/ueiQOUEl8vaA28v0uAIL2cIMxJY7mrii9D3NNip8d/iPzpgDZJk2epBClfpyw==} engines: {node: '>=18'} - '@sentry-internal/replay-canvas@9.25.0': - resolution: {integrity: sha512-eNjfS40OyU1Ca74YmDRm8PlLmwIH4N0EyIw7FScc92cr7ip+Y4UzRDEa2zJGwHPPuTRXexUI3vaZqmMQkWQP1g==} + '@sentry-internal/replay-canvas@9.26.0': + resolution: {integrity: sha512-ABj5TRRI3WWgLFPHrncCLOL5On/K+TpsbwWCM58AXQwwvtsSN2R22RY0ftuYgmAzBt4tygUJ9VQfIAWcRtC5sQ==} engines: {node: '>=18'} - '@sentry-internal/replay@9.25.0': - resolution: {integrity: sha512-aSk4cUv8KasQd8Gb2NHDH/c6IHRZwTq4gx9oo5rCYzMAHRQGNjGU18ecHOtLKKueQGCfrmF1Xv76LgjJVYsVOw==} + '@sentry-internal/replay@9.26.0': + resolution: {integrity: sha512-SrND17u9Of0Jal4i9fJLoi98puBU3CQxwWq1Vda5JI9nLNwVU00QRbcsXsiartp/e0A8m0yGsySlrAGb1tZTaA==} engines: {node: '>=18'} '@sentry/babel-plugin-component-annotate@3.5.0': resolution: {integrity: sha512-s2go8w03CDHbF9luFGtBHKJp4cSpsQzNVqgIa9Pfa4wnjipvrK6CxVT4icpLA3YO6kg5u622Yoa5GF3cJdippw==} engines: {node: '>= 14'} - '@sentry/browser@9.25.0': - resolution: {integrity: sha512-IkeGKrTX2nX0POgZATLiYJEIyjcwtf5z40fvuSofVSnONrnSuJmlkDI2grRLX+OhQh4MJaq8gwPhTMqf9koRTQ==} + '@sentry/browser@9.26.0': + resolution: {integrity: sha512-aZFAXcNtJe+QQidIiB8wW8uyzBnIJR81CoeZkDxl1fJ0YlAZraazyD35DWP7suLKujCPtWNv3vRzSxYMxxP/NQ==} engines: {node: '>=18'} '@sentry/bundler-plugin-core@3.5.0': @@ -2846,22 +2841,22 @@ packages: engines: {node: '>= 10'} hasBin: true - '@sentry/core@9.25.0': - resolution: {integrity: sha512-k0AgzR6RIf6OEwkVz09zer8GcK1s7RothlS1R6Z4x1wAJ+brtx4HqWnbLp05LDNDNrjTzK30HXvuCGGusnZuig==} + '@sentry/core@9.26.0': + resolution: {integrity: sha512-XTFSqOPn6wsZgF3NLRVY/FjYCkFahZoR46BtLVmBliD60QZLChpya81slD3M8BgLQpjsA2q6N1xrQor1Rc29gg==} engines: {node: '>=18'} - '@sentry/nextjs@9.25.0': - resolution: {integrity: sha512-XPTD4aX+NLn8N3JZJ3tW8o+leclL7v9N8jBqH9byay6iXaJxdKi27dokFs2GpwqIAZAqm3RIihECKtUnv8utmQ==} + '@sentry/nextjs@9.26.0': + resolution: {integrity: sha512-baOIDZT98rgaYUF1ZY3nrlQhZF3TwvksxxekJnZ1uuZp6FfDEvfK+4ruJDlWlcblv/aNPJLRhI22xAyUsS1cMg==} engines: {node: '>=18'} peerDependencies: next: ^13.2.0 || ^14.0 || ^15.0.0-rc.0 - '@sentry/node@9.25.0': - resolution: {integrity: sha512-Z7nkj7kwH1/kbsETmNN12pMD3Npe9X0bCKV3jlTv6KkEdVvklc1+/pT7Bz+4iYqHUysZTrNomQxdzjcQbIb2aw==} + '@sentry/node@9.26.0': + resolution: {integrity: sha512-B7VdUtXlg1Y8DeZMWc9gOIoSmGT9hkKepits+kmkZgjYlyPhZtT8a0fwUNBLYFYq1Ti/JzKWw3ZNIlg00BY40w==} engines: {node: '>=18'} - '@sentry/opentelemetry@9.25.0': - resolution: {integrity: sha512-yzl/DnlQMkpOsEHlZJeTXdJ8GJNyonUjM+d3jhAXDjsvG2yXXBrda0PhNkxCN+rScbP/sJEbvfGPtcnnysh7NA==} + '@sentry/opentelemetry@9.26.0': + resolution: {integrity: sha512-yVxRv6GtrtKFfNKpfb+b/focF4cKslInIN+HPzllQBoVebrq+KeCjUYzDEj9b6OwZGbUZDbQdxGRgXrrxcZUMg==} engines: {node: '>=18'} peerDependencies: '@opentelemetry/api': ^1.9.0 @@ -2871,14 +2866,14 @@ packages: '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.0.0 '@opentelemetry/semantic-conventions': ^1.34.0 - '@sentry/react@9.25.0': - resolution: {integrity: sha512-J7IXIubVl09lNVQy7xO7xLrTgL6SNe4aZPBw5j7aUF5MrskloCtJ86C20LMo8X+x1ZOoHshSFUdv1dt3ayDt7g==} + '@sentry/react@9.26.0': + resolution: {integrity: sha512-I2AreDlNK6bak5eRRLCRnphJTx8mrhXEZ0MiUMAbg0fcQ5kM/yf4C6LNJpMPa86UhBQDkXQus+Cb1uYQYNF7og==} engines: {node: '>=18'} peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x - '@sentry/vercel-edge@9.25.0': - resolution: {integrity: sha512-4bkJU6bJRx8qrabMqFHBk2IHmNpd89eRiS7Vr8u9QKsc/6cwqNRwpZnqw6znfkNFCtPIbXf+6FpP2xrhzGB3yA==} + '@sentry/vercel-edge@9.26.0': + resolution: {integrity: sha512-CO18exGB8fClwXxFqz9TpWYSQkq+fIqF1DZx3gLEsbt8x3giw5Fk4/itoBaQUBctXLa6+X9kQm5lSvf+wRkonQ==} engines: {node: '>=18'} '@sentry/webpack-plugin@3.5.0': @@ -5850,8 +5845,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-react@0.511.0: - resolution: {integrity: sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==} + lucide-react@0.513.0: + resolution: {integrity: sha512-CJZKq2g8Y8yN4Aq002GahSXbG2JpFv9kXwyiOAMvUBv7pxeOFHUWKB0mO7MiY4ZVFCV4aNjv2BJFq/z3DgKPQg==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -7745,8 +7740,8 @@ packages: peerDependencies: zod: ^3.18.0 - zod@3.25.49: - resolution: {integrity: sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q==} + zod@3.25.51: + resolution: {integrity: sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg==} snapshots: @@ -8581,10 +8576,10 @@ snapshots: - '@chromatic-com/playwright' - react - '@clerk/backend@1.34.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@clerk/backend@2.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@clerk/shared': 3.9.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@clerk/types': 4.59.3 + '@clerk/shared': 3.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@clerk/types': 4.60.0 cookie: 1.0.2 snakecase-keys: 8.0.1 tslib: 2.8.1 @@ -8592,31 +8587,29 @@ snapshots: - react - react-dom - '@clerk/clerk-react@5.31.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@clerk/clerk-react@5.31.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@clerk/shared': 3.9.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@clerk/types': 4.59.3 + '@clerk/shared': 3.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@clerk/types': 4.60.0 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) tslib: 2.8.1 - '@clerk/nextjs@6.20.2(next@15.3.2(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@clerk/nextjs@6.21.0(next@15.3.2(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@clerk/backend': 1.34.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@clerk/clerk-react': 5.31.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@clerk/shared': 3.9.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@clerk/types': 4.59.3 + '@clerk/backend': 2.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@clerk/clerk-react': 5.31.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@clerk/shared': 3.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@clerk/types': 4.60.0 next: 15.3.2(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) server-only: 0.0.1 tslib: 2.8.1 - transitivePeerDependencies: - - svix - '@clerk/shared@3.9.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@clerk/shared@3.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@clerk/types': 4.59.3 + '@clerk/types': 4.60.0 dequal: 2.0.3 glob-to-regexp: 0.4.1 js-cookie: 3.0.5 @@ -8626,12 +8619,12 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@clerk/themes@2.2.48': + '@clerk/themes@2.2.49': dependencies: - '@clerk/types': 4.59.3 + '@clerk/types': 4.60.0 tslib: 2.8.1 - '@clerk/types@4.59.3': + '@clerk/types@4.60.0': dependencies: csstype: 3.1.3 @@ -10307,33 +10300,33 @@ snapshots: '@rushstack/eslint-patch@1.10.5': {} - '@sentry-internal/browser-utils@9.25.0': + '@sentry-internal/browser-utils@9.26.0': dependencies: - '@sentry/core': 9.25.0 + '@sentry/core': 9.26.0 - '@sentry-internal/feedback@9.25.0': + '@sentry-internal/feedback@9.26.0': dependencies: - '@sentry/core': 9.25.0 + '@sentry/core': 9.26.0 - '@sentry-internal/replay-canvas@9.25.0': + '@sentry-internal/replay-canvas@9.26.0': dependencies: - '@sentry-internal/replay': 9.25.0 - '@sentry/core': 9.25.0 + '@sentry-internal/replay': 9.26.0 + '@sentry/core': 9.26.0 - '@sentry-internal/replay@9.25.0': + '@sentry-internal/replay@9.26.0': dependencies: - '@sentry-internal/browser-utils': 9.25.0 - '@sentry/core': 9.25.0 + '@sentry-internal/browser-utils': 9.26.0 + '@sentry/core': 9.26.0 '@sentry/babel-plugin-component-annotate@3.5.0': {} - '@sentry/browser@9.25.0': + '@sentry/browser@9.26.0': dependencies: - '@sentry-internal/browser-utils': 9.25.0 - '@sentry-internal/feedback': 9.25.0 - '@sentry-internal/replay': 9.25.0 - '@sentry-internal/replay-canvas': 9.25.0 - '@sentry/core': 9.25.0 + '@sentry-internal/browser-utils': 9.26.0 + '@sentry-internal/feedback': 9.26.0 + '@sentry-internal/replay': 9.26.0 + '@sentry-internal/replay-canvas': 9.26.0 + '@sentry/core': 9.26.0 '@sentry/bundler-plugin-core@3.5.0': dependencies: @@ -10389,19 +10382,19 @@ snapshots: - encoding - supports-color - '@sentry/core@9.25.0': {} + '@sentry/core@9.26.0': {} - '@sentry/nextjs@9.25.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.2(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.98.0(esbuild@0.25.2))': + '@sentry/nextjs@9.26.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.2(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.98.0(esbuild@0.25.2))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.34.0 '@rollup/plugin-commonjs': 28.0.1(rollup@4.35.0) - '@sentry-internal/browser-utils': 9.25.0 - '@sentry/core': 9.25.0 - '@sentry/node': 9.25.0 - '@sentry/opentelemetry': 9.25.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0) - '@sentry/react': 9.25.0(react@19.1.0) - '@sentry/vercel-edge': 9.25.0 + '@sentry-internal/browser-utils': 9.26.0 + '@sentry/core': 9.26.0 + '@sentry/node': 9.26.0 + '@sentry/opentelemetry': 9.26.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0) + '@sentry/react': 9.26.0(react@19.1.0) + '@sentry/vercel-edge': 9.26.0 '@sentry/webpack-plugin': 3.5.0(webpack@5.98.0(esbuild@0.25.2)) chalk: 3.0.0 next: 15.3.2(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -10418,7 +10411,7 @@ snapshots: - supports-color - webpack - '@sentry/node@9.25.0': + '@sentry/node@9.26.0': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) @@ -10450,14 +10443,14 @@ snapshots: '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.34.0 '@prisma/instrumentation': 6.8.2(@opentelemetry/api@1.9.0) - '@sentry/core': 9.25.0 - '@sentry/opentelemetry': 9.25.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0) + '@sentry/core': 9.26.0 + '@sentry/opentelemetry': 9.26.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0) import-in-the-middle: 1.13.2 minimatch: 9.0.5 transitivePeerDependencies: - supports-color - '@sentry/opentelemetry@9.25.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)': + '@sentry/opentelemetry@9.26.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) @@ -10465,19 +10458,19 @@ snapshots: '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.34.0 - '@sentry/core': 9.25.0 + '@sentry/core': 9.26.0 - '@sentry/react@9.25.0(react@19.1.0)': + '@sentry/react@9.26.0(react@19.1.0)': dependencies: - '@sentry/browser': 9.25.0 - '@sentry/core': 9.25.0 + '@sentry/browser': 9.26.0 + '@sentry/core': 9.26.0 hoist-non-react-statics: 3.3.2 react: 19.1.0 - '@sentry/vercel-edge@9.25.0': + '@sentry/vercel-edge@9.26.0': dependencies: '@opentelemetry/api': 1.9.0 - '@sentry/core': 9.25.0 + '@sentry/core': 9.26.0 '@sentry/webpack-plugin@3.5.0(webpack@5.98.0(esbuild@0.25.2))': dependencies: @@ -10844,19 +10837,19 @@ snapshots: dependencies: tslib: 2.8.1 - '@t3-oss/env-core@0.13.6(arktype@2.1.20)(typescript@5.8.3)(zod@3.25.49)': + '@t3-oss/env-core@0.13.6(arktype@2.1.20)(typescript@5.8.3)(zod@3.25.51)': optionalDependencies: arktype: 2.1.20 typescript: 5.8.3 - zod: 3.25.49 + zod: 3.25.51 - '@t3-oss/env-nextjs@0.13.6(arktype@2.1.20)(typescript@5.8.3)(zod@3.25.49)': + '@t3-oss/env-nextjs@0.13.6(arktype@2.1.20)(typescript@5.8.3)(zod@3.25.51)': dependencies: - '@t3-oss/env-core': 0.13.6(arktype@2.1.20)(typescript@5.8.3)(zod@3.25.49) + '@t3-oss/env-core': 0.13.6(arktype@2.1.20)(typescript@5.8.3)(zod@3.25.51) optionalDependencies: arktype: 2.1.20 typescript: 5.8.3 - zod: 3.25.49 + zod: 3.25.51 '@tailwindcss/node@4.1.8': dependencies: @@ -13959,8 +13952,8 @@ snapshots: smol-toml: 1.3.3 strip-json-comments: 5.0.1 typescript: 5.8.3 - zod: 3.25.49 - zod-validation-error: 3.4.0(zod@3.25.49) + zod: 3.25.51 + zod-validation-error: 3.4.0(zod@3.25.51) language-subtag-registry@0.3.23: {} @@ -14070,7 +14063,7 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@0.511.0(react@19.1.0): + lucide-react@0.513.0(react@19.1.0): dependencies: react: 19.1.0 @@ -16066,8 +16059,8 @@ snapshots: yocto-queue@1.2.1: {} - zod-validation-error@3.4.0(zod@3.25.49): + zod-validation-error@3.4.0(zod@3.25.51): dependencies: - zod: 3.25.49 + zod: 3.25.51 - zod@3.25.49: {} + zod@3.25.51: {} diff --git a/public/generate the image of a waiter, Ask Jeeves style, black and white, 1920s look and feel - chatgpt.png b/public/generate the image of a waiter, Ask Jeeves style, black and white, 1920s look and feel - chatgpt.png new file mode 100644 index 00000000..36ca0dfe Binary files /dev/null and b/public/generate the image of a waiter, Ask Jeeves style, black and white, 1920s look and feel - chatgpt.png differ diff --git a/public/generate the image of a waiter, Ask Jeeves style, black and white, 1920s look and feel, remove background - copilot.png b/public/generate the image of a waiter, Ask Jeeves style, black and white, 1920s look and feel, remove background - copilot.png new file mode 100644 index 00000000..e2a693bb Binary files /dev/null and b/public/generate the image of a waiter, Ask Jeeves style, black and white, 1920s look and feel, remove background - copilot.png differ diff --git a/src/domain/clerk.ts b/src/domain/clerk.ts index 425f7933..efa2d4ff 100644 --- a/src/domain/clerk.ts +++ b/src/domain/clerk.ts @@ -1,5 +1,14 @@ import { z } from 'zod'; +/** + * @see https://clerk.com/docs/backend-requests/resources/session-tokens > Version 2. + */ +export type ClerkSessionClaimsV2 = { + o: { + id: string; + }; +}; + export type ClerkOrganizationId = `org_${string}`; export const clerkOrgIdPrefix = (() => { const testValue: ClerkOrganizationId = 'org_'; diff --git a/src/domain/locations.ts b/src/domain/locations.ts index e4349f6e..ac7fad5a 100644 --- a/src/domain/locations.ts +++ b/src/domain/locations.ts @@ -12,7 +12,17 @@ export type Location = Omit, 'menuMode' | 'cu export type NewLocation = InferInsertModel; export type LocationId = Location['id']; -export const locationIdSchema = z.custom(); +export const locationIdSchema = z + .union([ + z.number().int().min(1), + z + .string() + .regex(/^\d+$/) + .transform((val) => parseInt(val, 10)), + ]) + .refine((val) => Number.isSafeInteger(val) && val > 0, { + message: 'Location ID must be a positive integer', + }); export const LOCATION_SLUG_LENGTH = 8; export const locationSlugSchema = z.coerce diff --git a/src/lib/location-utils.test.ts b/src/lib/location-utils.test.ts new file mode 100644 index 00000000..8187661d --- /dev/null +++ b/src/lib/location-utils.test.ts @@ -0,0 +1,66 @@ +import { AppError } from '~/lib/error-utils.server'; +import { getValidLocationId, getValidLocationIdOrThrow } from './location-utils'; + +describe('location-utils', () => { + describe('getValidLocationId', () => { + it('should return null when no candidate is provided', () => { + expect(getValidLocationId()).toBeNull(); + }); + + it('should return null when candidate is empty string', () => { + expect(getValidLocationId('')).toBeNull(); + }); + + it('should return null when candidate is invalid', () => { + expect(getValidLocationId('invalid-location-id')).toBeNull(); + expect(getValidLocationId('abc')).toBeNull(); + expect(getValidLocationId('!@#$')).toBeNull(); + expect(getValidLocationId('0')).toBeNull(); + expect(getValidLocationId('-1')).toBeNull(); + expect(getValidLocationId('1.5')).toBeNull(); + }); + + it('should return the parsed location ID when candidate is a valid integer passed as string', () => { + const validLocationId = '123'; + const result = getValidLocationId(validLocationId); + expect(result).toBe(123); + }); + + it('should return the parsed location ID when candidate is a valid integer ', () => { + const validLocationId = 123; + const result = getValidLocationId(validLocationId); + expect(result).toBe(123); + }); + }); + + describe('getValidLocationIdOrThrow', () => { + it('should throw AppError when no candidate is provided', () => { + expect(() => getValidLocationIdOrThrow()).toThrow(AppError); + }); + + it('should throw AppError when candidate is empty string', () => { + expect(() => getValidLocationIdOrThrow('')).toThrow(AppError); + }); + + it('should throw AppError when candidate is invalid', () => { + expect(() => getValidLocationIdOrThrow('invalid-location-id')).toThrow(AppError); + expect(() => getValidLocationIdOrThrow('abc')).toThrow(AppError); + expect(() => getValidLocationIdOrThrow('!@#$')).toThrow(AppError); + expect(() => getValidLocationIdOrThrow('0')).toThrow(AppError); + expect(() => getValidLocationIdOrThrow('-1')).toThrow(AppError); + expect(() => getValidLocationIdOrThrow('1.5')).toThrow(AppError); + }); + + it('should return the parsed location ID when candidate is a valid integer passed as string', () => { + const validLocationId = '123'; + const result = getValidLocationIdOrThrow(validLocationId); + expect(result).toBe(123); + }); + + it('should return the parsed location ID when candidate is a valid integer ', () => { + const validLocationId = 123; + const result = getValidLocationIdOrThrow(validLocationId); + expect(result).toBe(123); + }); + }); +}); diff --git a/src/lib/location-utils.ts b/src/lib/location-utils.ts index e6149022..fc46c339 100644 --- a/src/lib/location-utils.ts +++ b/src/lib/location-utils.ts @@ -1,13 +1,26 @@ import { type LocationId, locationIdSchema } from '~/domain/locations'; import { AppError } from '~/lib/error-utils.server'; -export function getValidLocationIdOrThrow(candidate?: string): LocationId { - const locationValidationResult = locationIdSchema.safeParse(candidate); - if (!locationValidationResult.success) { +export function getValidLocationIdOrThrow(candidate?: string | number): LocationId { + const parsedLocationId = getValidLocationId(candidate); + if (parsedLocationId == null) { throw new AppError({ internalMessage: `Location validation failed. params: ${JSON.stringify(candidate)}`, }); } + return parsedLocationId; +} + +export function getValidLocationId(candidate?: string | number): LocationId | null { + if (!candidate) { + return null; + } + + const locationValidationResult = locationIdSchema.safeParse(candidate); + if (!locationValidationResult.success) { + return null; + } + const parsedlocationId = locationValidationResult.data; return parsedlocationId; } diff --git a/src/middleware.ts b/src/middleware.ts index ef65f882..1772556e 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,7 +1,7 @@ import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; import { type NextRequest, NextResponse } from 'next/server'; import { CookieKey } from '~/domain/cookies'; -import { locationIdSchema } from '~/domain/locations'; +import { getValidLocationId } from '~/lib/location-utils'; import { getValidPriceTier } from '~/lib/price-tier-utils'; import { ROUTES } from '~/lib/routes'; @@ -17,8 +17,7 @@ const isAuthProtectedRoute = createRouteMatcher([`${ROUTES.userRoot}(.*)`]); export default clerkMiddleware( async (auth, req: NextRequest) => { - const { userId, sessionClaims, redirectToSignIn } = await auth(); - const orgId = sessionClaims?.org_id; + const { userId, orgId, sessionClaims, redirectToSignIn } = await auth(); if (isSignUpRoute(req)) { console.log(`DBG-MDLW [${req.url}] Is sign-up route`); @@ -47,18 +46,22 @@ export default clerkMiddleware( } const currentLocationId = req.cookies.get(CookieKey.CurrentLocationId)?.value; - const currentLocationValidationResult = locationIdSchema.safeParse(currentLocationId); - if (currentLocationValidationResult.success) { - const myDashboardRoute = ROUTES.live(currentLocationValidationResult.data); + const validCurrentLocationId = getValidLocationId(currentLocationId); + if (validCurrentLocationId) { + const myDashboardRoute = ROUTES.live(validCurrentLocationId); console.log(`DBG-MDLW [/my] Redirecting from ${req.url} to ${myDashboardRoute}`); return redirectTo(req, myDashboardRoute); } else { // Fall back to the initial location id in the session claims - const initialLocationId = sessionClaims?.metadata?.initialLocationId; - if (initialLocationId) { - const fallbackMyDashboardRoute = ROUTES.live(Number(initialLocationId)); + const validInitialLocationId = getValidLocationId(sessionClaims?.metadata?.initialLocationId); + if (validInitialLocationId) { + const fallbackMyDashboardRoute = ROUTES.live(Number(validInitialLocationId)); const fallbackResponse = redirectTo(req, fallbackMyDashboardRoute); - fallbackResponse.cookies.set(CookieKey.CurrentLocationId, initialLocationId, cookieOptions); + fallbackResponse.cookies.set( + CookieKey.CurrentLocationId, + validInitialLocationId.toString(), + cookieOptions, + ); console.log( `DBG-MDLW [/my] Fall back to initial location id. Redirecting from ${req.url} to ${fallbackMyDashboardRoute}`, ); diff --git a/src/server/queries.ts b/src/server/queries.ts index 235caf89..234bbba7 100644 --- a/src/server/queries.ts +++ b/src/server/queries.ts @@ -6,17 +6,12 @@ import { db } from '~/server/db'; import { locations, organizations } from '~/server/db/schema'; export async function getMenusPlanUsage() { - const { userId, sessionClaims } = await auth(); + const { userId, orgId } = await auth(); if (!userId) { throw new AppError({ internalMessage: 'Unauthorized' }); } - const validClerkOrgId = getValidClerkOrgIdOrThrow(sessionClaims?.org_id); - if (!validClerkOrgId) { - throw new AppError({ - internalMessage: `No valid clerk org id found in session claims - ${JSON.stringify(sessionClaims)}.`, - }); - } + const validClerkOrgId = getValidClerkOrgIdOrThrow(orgId); const result = await db.query.menus.findMany({ where: (menus, { eq, and, exists }) => @@ -33,12 +28,12 @@ export async function getMenusPlanUsage() { } export async function getMenuItemsPlanUsage() { - const { userId, sessionClaims } = await auth(); + const { userId, orgId } = await auth(); if (!userId) { throw new AppError({ internalMessage: 'Unauthorized' }); } - const validClerkOrgId = getValidClerkOrgIdOrThrow(sessionClaims?.org_id); + const validClerkOrgId = getValidClerkOrgIdOrThrow(orgId); const result = await db.query.menuItems.findMany({ where: (menuItems, { eq, and, exists }) => diff --git a/src/server/queries/locations.ts b/src/server/queries/locations.ts index 25bbf770..21192765 100644 --- a/src/server/queries/locations.ts +++ b/src/server/queries/locations.ts @@ -8,10 +8,10 @@ import { type LocationId, type LocationSlug, type locationFormSchema, - locationIdSchema, } from '~/domain/locations'; import { getValidClerkOrgIdOrThrow } from '~/lib/clerk-utils'; import { AppError } from '~/lib/error-utils.server'; +import { getValidLocationIdOrThrow } from '~/lib/location-utils'; import { db } from '~/server/db'; import { locations, organizations } from '~/server/db/schema'; @@ -57,23 +57,14 @@ export async function generateUniqueLocationSlug(): Promise { * @returns A valid Location. */ export async function getLocationForCurrentUserOrThrow(locationId: string | number): Promise { - const locationIdValidationResult = locationIdSchema.safeParse(locationId); - if (!locationIdValidationResult.success) { - throw new AppError({ internalMessage: `Invalid locationId: ${locationId}` }); - } - const validLocationId = locationIdValidationResult.data; + const validLocationId = getValidLocationIdOrThrow(locationId); - const { userId, sessionClaims } = await auth(); + const { userId, orgId } = await auth(); if (!userId) { throw new AppError({ internalMessage: 'Unauthorized - no user ID provided' }); } - const validClerkOrgId = getValidClerkOrgIdOrThrow(sessionClaims?.org_id); - if (!validClerkOrgId) { - throw new AppError({ - internalMessage: `Invalid organization ID: ${sessionClaims?.org_id}`, - }); - } + const validClerkOrgId = getValidClerkOrgIdOrThrow(orgId); const location = await db.query.locations.findFirst({ where: (locations, { and, eq }) => diff --git a/src/server/queries/menus.ts b/src/server/queries/menus.ts index 390b5ac2..c1edf68e 100644 --- a/src/server/queries/menus.ts +++ b/src/server/queries/menus.ts @@ -174,13 +174,13 @@ export async function getMenuById(locationId: LocationId, menuId: MenuId): Promi } export async function getMenusByLocation(locationId: LocationId): Promise { - const { userId, sessionClaims } = await auth(); + const { userId, orgId } = await auth(); if (!userId) { throw new AppError({ internalMessage: 'Unauthorized' }); } const validLocation = await getLocationForCurrentUserOrThrow(locationId); - const validClerkOrgId = getValidClerkOrgIdOrThrow(sessionClaims?.org_id); + const validClerkOrgId = getValidClerkOrgIdOrThrow(orgId); const menus = await db.query.menus.findMany({ where: (menus, { eq, and }) =>