Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
810c0eb61b | ||
| 3314063e25 |
@@ -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: |
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
186
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user