Compare commits

...

2 Commits

Author SHA1 Message Date
Gitea Actions
810c0eb61b ci: Bump version to 0.12.3 [skip ci] 2026-01-21 23:08:48 +05:00
3314063e25 migration from react-joyride to driver.js:
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 18m52s
2026-01-21 10:07:38 -08:00
8 changed files with 312 additions and 271 deletions

View File

@@ -45,7 +45,7 @@ jobs:
cache-dependency-path: '**/package-lock.json'
- name: Install Dependencies
run: npm ci --legacy-peer-deps
run: npm ci
- name: Bump Minor Version and Push
run: |

View File

@@ -41,7 +41,7 @@ jobs:
# If dependencies are not found in cache, it will run 'npm ci' automatically.
# If they are found, it restores them. This is the standard, reliable way.
- name: Install Dependencies
run: npm ci --legacy-peer-deps # 'ci' is faster and safer for CI/CD than 'install'.
run: npm ci # 'ci' is faster and safer for CI/CD than 'install'.
- name: Bump Version and Push
run: |

View File

@@ -340,8 +340,7 @@ WORKDIR /app
COPY package*.json ./
# Install all dependencies (including devDependencies for development)
# Use --legacy-peer-deps due to react-joyride peer dependency conflict with React 19
RUN npm install --legacy-peer-deps
RUN npm install
# ============================================================================
# Environment Configuration

View File

@@ -135,7 +135,7 @@ New users saw "Welcome to Flyer Crawler!" with no explanation of features or how
### Solution
Implemented interactive guided tour using `react-joyride`:
Implemented interactive guided tour using `driver.js` (framework-agnostic, React 19 compatible):
**Tour Steps** (6 total):
@@ -148,24 +148,27 @@ Implemented interactive guided tour using `react-joyride`:
**Features**:
- Auto-starts for first-time users
- Auto-starts for first-time users (500ms delay for DOM readiness)
- Persists completion in localStorage (`flyer_crawler_onboarding_completed`)
- Skip button for experienced users
- Progress indicator showing current step
- Styled with brand colors (#14b8a6)
- Custom styled with pastel colors, sharp borders (design system)
- Dark mode compatible
- Zero React peer dependencies (compatible with React 19)
### Deliverables
- **Created**: `src/hooks/useOnboardingTour.ts` (custom hook)
- **Created**: `src/hooks/useOnboardingTour.ts` (custom hook with Driver.js)
- **Modified**: Added `data-tour` attributes to 6 components:
- `src/features/flyer/FlyerUploader.tsx`
- `src/features/flyer/ExtractedDataTable.tsx`
- `src/features/shopping/WatchedItemsList.tsx`
- `src/features/charts/PriceChart.tsx`
- `src/features/shopping/ShoppingList.tsx`
- **Modified**: `src/layouts/MainLayout.tsx` - Integrated Joyride component
- **Installed**: `react-joyride@2.9.3`, `@types/react-joyride@2.0.2`
- **Modified**: `src/layouts/MainLayout.tsx` - Integrated tour via hook
- **Installed**: `driver.js@^1.3.1`
**Migration Note (2026-01-21)**: Originally implemented with `react-joyride@2.9.3`, but migrated to `driver.js` for React 19 compatibility.
### User Flow
@@ -360,13 +363,13 @@ npm test -- --run src/components/Button.test.tsx
1. `tailwind.config.js` - Brand colors
2. `src/App.tsx` - New routes, MobileTabBar
3. `src/layouts/MainLayout.tsx` - Joyride, responsive layout
3. `src/layouts/MainLayout.tsx` - Tour integration, responsive layout
4. `src/features/flyer/FlyerUploader.tsx` - Button, data-tour
5. `src/features/flyer/ExtractedDataTable.tsx` - data-tour
6. `src/features/shopping/WatchedItemsList.tsx` - Button, data-tour
7. `src/features/shopping/ShoppingList.tsx` - Button, data-tour
8. `src/features/charts/PriceChart.tsx` - data-tour
9. `package.json` - Dependencies (react-joyride)
9. `package.json` - Dependencies (driver.js)
10. `package-lock.json` - Dependency lock
### Statistics
@@ -383,7 +386,7 @@ npm test -- --run src/components/Button.test.tsx
### Bundle Size Impact
- `react-joyride`: ~30KB gzipped
- `driver.js`: ~10KB gzipped (lightweight, zero dependencies)
- `Button` component: <5KB (reduces duplication)
- Brand colors: 0KB (CSS utilities, tree-shaken)
- **Total increase**: ~25KB gzipped
@@ -461,8 +464,9 @@ If issues arise:
1. Revert commit containing `src/components/MobileTabBar.tsx`
2. Remove new routes from `src/App.tsx`
3. Restore previous `MainLayout.tsx` (remove Joyride)
3. Restore previous `MainLayout.tsx` (remove tour integration)
4. Keep Button component and brand colors (safe changes)
5. Remove `driver.js` and restore localStorage keys if needed
---

186
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "flyer-crawler",
"version": "0.12.2",
"version": "0.12.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "flyer-crawler",
"version": "0.12.2",
"version": "0.12.3",
"dependencies": {
"@bull-board/api": "^6.14.2",
"@bull-board/express": "^6.14.2",
@@ -15,12 +15,12 @@
"@sentry/react": "^10.32.1",
"@tanstack/react-query": "^5.90.12",
"@types/connect-timeout": "^1.9.0",
"@types/react-joyride": "^2.0.2",
"bcrypt": "^5.1.1",
"bullmq": "^5.65.1",
"connect-timeout": "^1.9.1",
"cookie-parser": "^1.4.7",
"date-fns": "^4.1.0",
"driver.js": "^1.3.1",
"exif-parser": "^0.1.12",
"express": "^5.1.0",
"express-list-endpoints": "^7.1.1",
@@ -45,7 +45,6 @@
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0",
"react-joyride": "^2.9.3",
"react-router-dom": "^7.9.6",
"recharts": "^3.4.1",
"sharp": "^0.34.5",
@@ -2144,12 +2143,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@gilbarbara/deep-equal": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.3.1.tgz",
"integrity": "sha512-I7xWjLs2YSVMc5gGx1Z3ZG1lgFpITPndpi8Ku55GeEIKpACCPQNS/OTqQbxgTCfq0Ncvcc+CrFov96itVh6Qvw==",
"license": "MIT"
},
"node_modules/@google/genai": {
"version": "1.34.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.34.0.tgz",
@@ -6603,6 +6596,7 @@
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -6618,15 +6612,6 @@
"@types/react": "^19.2.0"
}
},
"node_modules/@types/react-joyride": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@types/react-joyride/-/react-joyride-2.0.2.tgz",
"integrity": "sha512-RbixI8KE4K4B4bVzigT765oiQMCbWqlb9vj5qz1pFvkOvynkiAGurGVVf+nGszGGa89WrQhUnAwd0t1tqxeoDw==",
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
@@ -9354,13 +9339,6 @@
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/deep-diff": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz",
"integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"license": "MIT"
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -9368,15 +9346,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/default-require-extensions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz",
@@ -9625,6 +9594,12 @@
"url": "https://dotenvx.com"
}
},
"node_modules/driver.js": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.4.0.tgz",
"integrity": "sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew==",
"license": "MIT"
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -12186,12 +12161,6 @@
"node": ">=0.10.0"
}
},
"node_modules/is-lite": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-lite/-/is-lite-1.2.1.tgz",
"integrity": "sha512-pgF+L5bxC+10hLBgf6R2P4ZZUBOQIIacbdo8YvuCP8/JvsWxG7aZ9p10DYuLtifFci4l3VITphhMlMV4Y+urPw==",
"license": "MIT"
},
"node_modules/is-map": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
@@ -12731,6 +12700,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@@ -13629,6 +13599,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@@ -14792,6 +14763,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
"license": "MIT",
"peer": true
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -15420,17 +15398,6 @@
"node": ">=8"
}
},
"node_modules/popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
"deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -15630,6 +15597,7 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@@ -15641,6 +15609,7 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true,
"license": "MIT"
},
"node_modules/proper-lockfile": {
@@ -15856,45 +15825,6 @@
"react": "^19.2.3"
}
},
"node_modules/react-floater": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/react-floater/-/react-floater-0.7.9.tgz",
"integrity": "sha512-NXqyp9o8FAXOATOEo0ZpyaQ2KPb4cmPMXGWkx377QtJkIXHlHRAGer7ai0r0C1kG5gf+KJ6Gy+gdNIiosvSicg==",
"license": "MIT",
"dependencies": {
"deepmerge": "^4.3.1",
"is-lite": "^0.8.2",
"popper.js": "^1.16.0",
"prop-types": "^15.8.1",
"tree-changes": "^0.9.1"
},
"peerDependencies": {
"react": "15 - 18",
"react-dom": "15 - 18"
}
},
"node_modules/react-floater/node_modules/@gilbarbara/deep-equal": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.1.2.tgz",
"integrity": "sha512-jk+qzItoEb0D0xSSmrKDDzf9sheQj/BAPxlgNxgmOaA3mxpUa6ndJLYGZKsJnIVEQSD8zcTbyILz7I0HcnBCRA==",
"license": "MIT"
},
"node_modules/react-floater/node_modules/is-lite": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/is-lite/-/is-lite-0.8.2.tgz",
"integrity": "sha512-JZfH47qTsslwaAsqbMI3Q6HNNjUuq6Cmzzww50TdP5Esb6e1y2sK2UAaZZuzfAzpoI2AkxoPQapZdlDuP6Vlsw==",
"license": "MIT"
},
"node_modules/react-floater/node_modules/tree-changes": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/tree-changes/-/tree-changes-0.9.3.tgz",
"integrity": "sha512-vvvS+O6kEeGRzMglTKbc19ltLWNtmNt1cpBoSYLj/iEcPVvpJasemKOlxBrmZaCtDJoF+4bwv3m01UKYi8mukQ==",
"license": "MIT",
"dependencies": {
"@gilbarbara/deep-equal": "^0.1.1",
"is-lite": "^0.8.2"
}
},
"node_modules/react-hot-toast": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
@@ -15912,64 +15842,12 @@
"react-dom": ">=16"
}
},
"node_modules/react-innertext": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/react-innertext/-/react-innertext-1.1.5.tgz",
"integrity": "sha512-PWAqdqhxhHIv80dT9znP2KvS+hfkbRovFp4zFYHFFlOoQLRiawIic81gKb3U1wEyJZgMwgs3JoLtwryASRWP3Q==",
"license": "MIT",
"peerDependencies": {
"@types/react": ">=0.0.0 <=99",
"react": ">=0.0.0 <=99"
}
},
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT"
},
"node_modules/react-joyride": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/react-joyride/-/react-joyride-2.9.3.tgz",
"integrity": "sha512-1+Mg34XK5zaqJ63eeBhqdbk7dlGCFp36FXwsEvgpjqrtyywX2C6h9vr3jgxP0bGHCw8Ilsp/nRDzNVq6HJ3rNw==",
"license": "MIT",
"dependencies": {
"@gilbarbara/deep-equal": "^0.3.1",
"deep-diff": "^1.0.2",
"deepmerge": "^4.3.1",
"is-lite": "^1.2.1",
"react-floater": "^0.7.9",
"react-innertext": "^1.1.5",
"react-is": "^16.13.1",
"scroll": "^3.0.1",
"scrollparent": "^2.1.0",
"tree-changes": "^0.11.2",
"type-fest": "^4.27.0"
},
"peerDependencies": {
"react": "15 - 18",
"react-dom": "15 - 18"
}
},
"node_modules/react-joyride/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-joyride/node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
@@ -16603,18 +16481,6 @@
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
"node_modules/scroll": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/scroll/-/scroll-3.0.1.tgz",
"integrity": "sha512-pz7y517OVls1maEzlirKO5nPYle9AXsFzTMNJrRGmT951mzpIBy7sNHOg5o/0MQd/NqliCiWnAi0kZneMPFLcg==",
"license": "MIT"
},
"node_modules/scrollparent": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/scrollparent/-/scrollparent-2.1.0.tgz",
"integrity": "sha512-bnnvJL28/Rtz/kz2+4wpBjHzWoEzXhVg/TE8BeVGJHUqE8THNIRnDxDWMktwM+qahvlRdvlLdsQfYe+cuqfZeA==",
"license": "ISC"
},
"node_modules/secure-json-parse": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz",
@@ -18065,16 +17931,6 @@
"node": ">=20"
}
},
"node_modules/tree-changes": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/tree-changes/-/tree-changes-0.11.3.tgz",
"integrity": "sha512-r14mvDZ6tqz8PRQmlFKjhUVngu4VZ9d92ON3tp0EGpFBE6PAHOq8Bx8m8ahbNoGE3uI/npjYcJiqVydyOiYXag==",
"license": "MIT",
"dependencies": {
"@gilbarbara/deep-equal": "^0.3.1",
"is-lite": "^1.2.1"
}
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "flyer-crawler",
"private": true,
"version": "0.12.2",
"version": "0.12.3",
"type": "module",
"scripts": {
"dev": "concurrently \"npm:start:dev\" \"vite\"",
@@ -36,7 +36,6 @@
"@sentry/react": "^10.32.1",
"@tanstack/react-query": "^5.90.12",
"@types/connect-timeout": "^1.9.0",
"@types/react-joyride": "^2.0.2",
"bcrypt": "^5.1.1",
"bullmq": "^5.65.1",
"connect-timeout": "^1.9.1",
@@ -66,7 +65,7 @@
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0",
"react-joyride": "^2.9.3",
"driver.js": "^1.3.1",
"react-router-dom": "^7.9.6",
"recharts": "^3.4.1",
"sharp": "^0.34.5",

View File

@@ -1,87 +1,286 @@
import { useState, useEffect, useCallback } from 'react';
import type { Step, CallBackProps } from 'react-joyride';
import { useEffect, useCallback, useRef } from 'react';
import { driver, Driver, DriveStep } from 'driver.js';
import 'driver.js/dist/driver.css';
const ONBOARDING_STORAGE_KEY = 'flyer_crawler_onboarding_completed';
export const useOnboardingTour = () => {
const [runTour, setRunTour] = useState(false);
const [stepIndex, setStepIndex] = useState(0);
// Custom CSS to match design system: pastel colors, sharp borders, minimalist
const DRIVER_CSS = `
.driver-popover {
background-color: #f0fdfa !important;
border: 2px solid #0d9488 !important;
border-radius: 0 !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
max-width: 320px !important;
}
.driver-popover-title {
color: #134e4a !important;
font-size: 1rem !important;
font-weight: 600 !important;
margin-bottom: 0.5rem !important;
}
.driver-popover-description {
color: #1f2937 !important;
font-size: 0.875rem !important;
line-height: 1.5 !important;
}
.driver-popover-progress-text {
color: #0d9488 !important;
font-size: 0.75rem !important;
font-weight: 500 !important;
}
.driver-popover-navigation-btns {
gap: 0.5rem !important;
}
.driver-popover-prev-btn,
.driver-popover-next-btn {
background-color: #14b8a6 !important;
color: white !important;
border: 1px solid #0d9488 !important;
border-radius: 0 !important;
padding: 0.5rem 1rem !important;
font-size: 0.875rem !important;
font-weight: 500 !important;
transition: background-color 0.15s ease !important;
}
.driver-popover-prev-btn:hover,
.driver-popover-next-btn:hover {
background-color: #115e59 !important;
}
.driver-popover-prev-btn {
background-color: #ccfbf1 !important;
color: #134e4a !important;
}
.driver-popover-prev-btn:hover {
background-color: #99f6e4 !important;
}
.driver-popover-close-btn {
color: #0d9488 !important;
font-size: 1.25rem !important;
}
.driver-popover-close-btn:hover {
color: #115e59 !important;
}
.driver-popover-arrow-side-left,
.driver-popover-arrow-side-right,
.driver-popover-arrow-side-top,
.driver-popover-arrow-side-bottom {
border-color: #0d9488 !important;
}
.driver-popover-arrow-side-left::after,
.driver-popover-arrow-side-right::after,
.driver-popover-arrow-side-top::after,
.driver-popover-arrow-side-bottom::after {
border-color: transparent !important;
}
.driver-popover-arrow-side-left::before {
border-right-color: #f0fdfa !important;
}
.driver-popover-arrow-side-right::before {
border-left-color: #f0fdfa !important;
}
.driver-popover-arrow-side-top::before {
border-bottom-color: #f0fdfa !important;
}
.driver-popover-arrow-side-bottom::before {
border-top-color: #f0fdfa !important;
}
.driver-overlay {
background-color: rgba(0, 0, 0, 0.5) !important;
}
.driver-active-element {
box-shadow: 0 0 0 4px #14b8a6 !important;
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.driver-popover {
background-color: #1f2937 !important;
border-color: #14b8a6 !important;
}
.driver-popover-title {
color: #ccfbf1 !important;
}
.driver-popover-description {
color: #e5e7eb !important;
}
.driver-popover-arrow-side-left::before {
border-right-color: #1f2937 !important;
}
.driver-popover-arrow-side-right::before {
border-left-color: #1f2937 !important;
}
.driver-popover-arrow-side-top::before {
border-bottom-color: #1f2937 !important;
}
.driver-popover-arrow-side-bottom::before {
border-top-color: #1f2937 !important;
}
}
`;
const tourSteps: DriveStep[] = [
{
element: '[data-tour="flyer-uploader"]',
popover: {
title: 'Upload Flyers',
description:
'Upload a grocery flyer here by clicking or dragging a PDF/image file. Our AI will extract prices and items automatically.',
side: 'bottom',
align: 'start',
},
},
{
element: '[data-tour="extracted-data-table"]',
popover: {
title: 'Extracted Items',
description:
'View all extracted items from your flyers here. You can watch items to track price changes and deals.',
side: 'top',
align: 'start',
},
},
{
element: '[data-tour="watch-button"]',
popover: {
title: 'Watch Items',
description:
'Click the eye icon to watch items and get notified when prices drop or deals appear.',
side: 'left',
align: 'start',
},
},
{
element: '[data-tour="watched-items"]',
popover: {
title: 'Watched Items',
description:
'Your watched items appear here. Track prices across different stores and get deal alerts.',
side: 'left',
align: 'start',
},
},
{
element: '[data-tour="price-chart"]',
popover: {
title: 'Active Deals',
description:
'Active deals show here with price comparisons. See which store has the best price!',
side: 'left',
align: 'start',
},
},
{
element: '[data-tour="shopping-list"]',
popover: {
title: 'Shopping Lists',
description:
'Create shopping lists from your watched items and get the best prices automatically.',
side: 'left',
align: 'start',
},
},
];
// Inject custom styles into the document head
const injectStyles = () => {
const styleId = 'driver-js-custom-styles';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = DRIVER_CSS;
document.head.appendChild(style);
}
};
export const useOnboardingTour = () => {
const driverRef = useRef<Driver | null>(null);
const markTourComplete = useCallback(() => {
localStorage.setItem(ONBOARDING_STORAGE_KEY, 'true');
}, []);
const startTour = useCallback(() => {
injectStyles();
if (driverRef.current) {
driverRef.current.destroy();
}
driverRef.current = driver({
showProgress: true,
steps: tourSteps,
nextBtnText: 'Next',
prevBtnText: 'Previous',
doneBtnText: 'Done',
progressText: 'Step {{current}} of {{total}}',
onDestroyed: () => {
markTourComplete();
},
});
driverRef.current.drive();
}, [markTourComplete]);
const skipTour = useCallback(() => {
if (driverRef.current) {
driverRef.current.destroy();
}
markTourComplete();
}, [markTourComplete]);
const replayTour = useCallback(() => {
startTour();
}, [startTour]);
// Auto-start tour on mount if not completed
useEffect(() => {
const hasCompletedOnboarding = localStorage.getItem(ONBOARDING_STORAGE_KEY);
if (!hasCompletedOnboarding) {
setRunTour(true);
// Small delay to ensure DOM elements are mounted
const timer = setTimeout(() => {
startTour();
}, 500);
return () => clearTimeout(timer);
}
}, []);
}, [startTour]);
const steps: Step[] = [
{
target: '[data-tour="flyer-uploader"]',
content:
'Upload a grocery flyer here by clicking or dragging a PDF/image file. Our AI will extract prices and items automatically.',
disableBeacon: true,
placement: 'bottom',
},
{
target: '[data-tour="extracted-data-table"]',
content:
'View all extracted items from your flyers here. You can watch items to track price changes and deals.',
placement: 'top',
},
{
target: '[data-tour="watch-button"]',
content:
'Click the eye icon to watch items and get notified when prices drop or deals appear.',
placement: 'left',
},
{
target: '[data-tour="watched-items"]',
content:
'Your watched items appear here. Track prices across different stores and get deal alerts.',
placement: 'left',
},
{
target: '[data-tour="price-chart"]',
content: 'Active deals show here with price comparisons. See which store has the best price!',
placement: 'left',
},
{
target: '[data-tour="shopping-list"]',
content:
'Create shopping lists from your watched items and get the best prices automatically.',
placement: 'left',
},
];
const handleJoyrideCallback = useCallback((data: CallBackProps) => {
const { status, index } = data;
if (status === 'finished' || status === 'skipped') {
localStorage.setItem(ONBOARDING_STORAGE_KEY, 'true');
setRunTour(false);
setStepIndex(0);
} else if (data.action === 'next' || data.action === 'prev') {
setStepIndex(index + (data.action === 'next' ? 1 : 0));
}
}, []);
const skipTour = useCallback(() => {
localStorage.setItem(ONBOARDING_STORAGE_KEY, 'true');
setRunTour(false);
setStepIndex(0);
}, []);
const replayTour = useCallback(() => {
setStepIndex(0);
setRunTour(true);
// Cleanup on unmount
useEffect(() => {
return () => {
if (driverRef.current) {
driverRef.current.destroy();
}
};
}, []);
return {
runTour,
steps,
stepIndex,
handleJoyrideCallback,
skipTour,
replayTour,
startTour,
};
};

View File

@@ -1,7 +1,6 @@
// src/layouts/MainLayout.tsx
import React, { useCallback } from 'react';
import { Outlet } from 'react-router-dom';
import Joyride from 'react-joyride';
import { useAuth } from '../hooks/useAuth';
import { useOnboardingTour } from '../hooks/useOnboardingTour';
import { useFlyers } from '../hooks/useFlyers';
@@ -34,7 +33,8 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
}) => {
const { userProfile, authStatus } = useAuth();
const user = userProfile?.user ?? null;
const { runTour, steps, stepIndex, handleJoyrideCallback } = useOnboardingTour();
// Driver.js tour is initialized and managed imperatively inside the hook
useOnboardingTour();
const { flyers, refetchFlyers, flyersError } = useFlyers();
const { masterItems, error: masterItemsError } = useMasterItems();
const {
@@ -99,22 +99,6 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
return (
<main className="max-w-screen-2xl mx-auto py-4 px-2.5 sm:py-6 lg:py-8">
<Joyride
steps={steps}
run={runTour}
stepIndex={stepIndex}
callback={handleJoyrideCallback}
continuous
showProgress
showSkipButton
styles={{
options: {
primaryColor: '#14b8a6',
textColor: '#1f2937',
zIndex: 10000,
},
}}
/>
{shouldShowBanner && (
<div className="max-w-5xl mx-auto mb-6 px-4 lg:px-0">
<AnonymousUserBanner onOpenProfile={onOpenProfile} />