diff --git a/.github/workflows/cicd-dev.yml b/.github/workflows/cicd-dev.yml
index 5bd0f4b..ccc31bd 100644
--- a/.github/workflows/cicd-dev.yml
+++ b/.github/workflows/cicd-dev.yml
@@ -22,8 +22,8 @@ jobs:
- name: Create environment file
run: |
- echo "NODE_ENV=dev" > .env
- echo "VITE_API_URL=${{ secrets.VITE_API_URL }}" >> .env
+ echo "NODE_ENV=production" > dist/.env
+ echo "VITE_API_URL=${{ secrets.VITE_API_URL }}" >> dist/.env
- name: Build the project
run: npm run build
diff --git a/package.json b/package.json
index a4a0ef2..54575d1 100644
--- a/package.json
+++ b/package.json
@@ -13,13 +13,13 @@
"dependencies": {
"@radix-ui/react-dialog": "^1.1.1",
"@tailwindcss/typography": "^0.5.15",
- "@tanstack/react-query": "^5.52.1",
"axios": "^1.7.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.427.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-ga4": "^2.1.0",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.26.0",
"tailwind-merge": "^2.4.0"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f8c2e1a..f12c244 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,13 +14,10 @@ importers:
dependencies:
'@radix-ui/react-dialog':
specifier: ^1.1.1
- version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tailwindcss/typography':
specifier: ^0.5.15
version: 0.5.15(tailwindcss@3.4.9)
- '@tanstack/react-query':
- specifier: ^5.52.1
- version: 5.52.1(react@18.3.1)
axios:
specifier: ^1.7.7
version: 1.7.7
@@ -39,12 +36,15 @@ importers:
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
+ react-ga4:
+ specifier: ^2.1.0
+ version: 2.1.0
react-markdown:
specifier: ^9.0.1
version: 9.0.1(@types/react@18.3.3)(react@18.3.1)
react-router-dom:
specifier: ^6.26.0
- version: 6.26.0(react-dom@18.3.1)(react@18.3.1)
+ version: 6.26.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
tailwind-merge:
specifier: ^2.4.0
version: 2.4.0
@@ -57,7 +57,7 @@ importers:
version: 6.5.0
'@testing-library/react':
specifier: ^16.0.0
- version: 16.0.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ version: 16.0.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@types/jest':
specifier: ^29.5.12
version: 29.5.12
@@ -72,7 +72,7 @@ importers:
version: 18.3.0
'@vitejs/plugin-react':
specifier: ^4.3.1
- version: 4.3.1(vite@5.4.0)
+ version: 4.3.1(vite@5.4.0(@types/node@22.2.0))
autoprefixer:
specifier: ^10.4.20
version: 10.4.20(postcss@8.4.41)
@@ -105,7 +105,7 @@ importers:
version: 1.0.7(tailwindcss@3.4.9)
ts-jest:
specifier: ^29.2.5
- version: 29.2.5(@babel/core@7.25.2)(jest@29.7.0)(typescript@5.5.3)
+ version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@22.2.0))(typescript@5.5.3)
typescript:
specifier: ^5.5.3
version: 5.5.3
@@ -862,14 +862,6 @@ packages:
peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20'
- '@tanstack/query-core@5.52.0':
- resolution: {integrity: sha512-U1DOEgltjUwalN6uWYTewSnA14b+tE7lSylOiASKCAO61ENJeCq9VVD/TXHA6O5u9+6v5+UgGYBSccTKDoyMqw==}
-
- '@tanstack/react-query@5.52.1':
- resolution: {integrity: sha512-soyn4dNIUZ8US8NaPVXv06gkZFHaZnPfKWPDjRJjFRW3Y7WZ0jx72eT6zhw3VQlkMPysmXye8l35ewPHspKgbQ==}
- peerDependencies:
- react: ^18 || ^19
-
'@testing-library/dom@10.4.0':
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
engines: {node: '>=18'}
@@ -2212,8 +2204,8 @@ packages:
micromark@4.0.0:
resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==}
- micromatch@4.0.7:
- resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==}
+ micromatch@4.0.8:
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mime-db@1.52.0:
@@ -2468,6 +2460,9 @@ packages:
peerDependencies:
react: ^18.3.1
+ react-ga4@2.1.0:
+ resolution: {integrity: sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==}
+
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
@@ -3416,7 +3411,7 @@ snapshots:
jest-util: 29.7.0
jest-validate: 29.7.0
jest-watcher: 29.7.0
- micromatch: 4.0.7
+ micromatch: 4.0.8
pretty-format: 29.7.0
slash: 3.0.0
strip-ansi: 6.0.1
@@ -3527,7 +3522,7 @@ snapshots:
jest-haste-map: 29.7.0
jest-regex-util: 29.6.3
jest-util: 29.7.0
- micromatch: 4.0.7
+ micromatch: 4.0.8
pirates: 4.0.6
slash: 3.0.0
write-file-atomic: 4.0.2
@@ -3579,121 +3574,136 @@ snapshots:
'@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.3)(react@18.3.1)':
dependencies:
- '@types/react': 18.3.3
react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.3
'@radix-ui/react-context@1.1.0(@types/react@18.3.3)(react@18.3.1)':
dependencies:
- '@types/react': 18.3.3
react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.3
- '@radix-ui/react-dialog@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)':
+ '@radix-ui/react-dialog@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1)
- '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.3)(react@18.3.1)
- '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-id': 1.1.0(@types/react@18.3.3)(react@18.3.1)
- '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
- '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
- '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1)
- '@types/react': 18.3.3
- '@types/react-dom': 18.3.0
aria-hidden: 1.2.4
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-remove-scroll: 2.5.7(@types/react@18.3.3)(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
- '@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)':
+ '@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
- '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.3)(react@18.3.1)
- '@types/react': 18.3.3
- '@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
'@radix-ui/react-focus-guards@1.1.0(@types/react@18.3.3)(react@18.3.1)':
dependencies:
- '@types/react': 18.3.3
react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.3
- '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)':
+ '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
- '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1)
- '@types/react': 18.3.3
- '@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
'@radix-ui/react-id@1.1.0(@types/react@18.3.3)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1)
- '@types/react': 18.3.3
react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.3
- '@radix-ui/react-portal@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)':
+ '@radix-ui/react-portal@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
- '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1)
- '@types/react': 18.3.3
- '@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
- '@radix-ui/react-presence@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)':
+ '@radix-ui/react-presence@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1)
- '@types/react': 18.3.3
- '@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
- '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)':
+ '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1)
- '@types/react': 18.3.3
- '@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
'@radix-ui/react-slot@1.1.0(@types/react@18.3.3)(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
- '@types/react': 18.3.3
react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.3
'@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.3)(react@18.3.1)':
dependencies:
- '@types/react': 18.3.3
react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.3
'@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.3)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1)
- '@types/react': 18.3.3
react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.3
'@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.3)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1)
- '@types/react': 18.3.3
react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.3
'@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.3)(react@18.3.1)':
dependencies:
- '@types/react': 18.3.3
react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.3
'@remix-run/router@1.19.0': {}
@@ -3763,13 +3773,6 @@ snapshots:
postcss-selector-parser: 6.0.10
tailwindcss: 3.4.9
- '@tanstack/query-core@5.52.0': {}
-
- '@tanstack/react-query@5.52.1(react@18.3.1)':
- dependencies:
- '@tanstack/query-core': 5.52.0
- react: 18.3.1
-
'@testing-library/dom@10.4.0':
dependencies:
'@babel/code-frame': 7.24.7
@@ -3791,14 +3794,15 @@ snapshots:
lodash: 4.17.21
redent: 3.0.0
- '@testing-library/react@16.0.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)':
+ '@testing-library/react@16.0.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.4
'@testing-library/dom': 10.4.0
- '@types/react': 18.3.3
- '@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
'@tootallnate/once@2.0.0': {}
@@ -3899,7 +3903,7 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
- '@typescript-eslint/eslint-plugin@8.0.0(@typescript-eslint/parser@8.0.0)(eslint@9.8.0)(typescript@5.5.3)':
+ '@typescript-eslint/eslint-plugin@8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.3))(eslint@9.8.0)(typescript@5.5.3)':
dependencies:
'@eslint-community/regexpp': 4.11.0
'@typescript-eslint/parser': 8.0.0(eslint@9.8.0)(typescript@5.5.3)
@@ -3912,6 +3916,7 @@ snapshots:
ignore: 5.3.2
natural-compare: 1.4.0
ts-api-utils: 1.3.0(typescript@5.5.3)
+ optionalDependencies:
typescript: 5.5.3
transitivePeerDependencies:
- supports-color
@@ -3924,6 +3929,7 @@ snapshots:
'@typescript-eslint/visitor-keys': 8.0.0
debug: 4.3.6
eslint: 9.8.0
+ optionalDependencies:
typescript: 5.5.3
transitivePeerDependencies:
- supports-color
@@ -3939,6 +3945,7 @@ snapshots:
'@typescript-eslint/utils': 8.0.0(eslint@9.8.0)(typescript@5.5.3)
debug: 4.3.6
ts-api-utils: 1.3.0(typescript@5.5.3)
+ optionalDependencies:
typescript: 5.5.3
transitivePeerDependencies:
- eslint
@@ -3956,6 +3963,7 @@ snapshots:
minimatch: 9.0.5
semver: 7.6.3
ts-api-utils: 1.3.0(typescript@5.5.3)
+ optionalDependencies:
typescript: 5.5.3
transitivePeerDependencies:
- supports-color
@@ -3978,7 +3986,7 @@ snapshots:
'@ungap/structured-clone@1.2.0': {}
- '@vitejs/plugin-react@4.3.1(vite@5.4.0)':
+ '@vitejs/plugin-react@4.3.1(vite@5.4.0(@types/node@22.2.0))':
dependencies:
'@babel/core': 7.25.2
'@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.25.2)
@@ -4532,7 +4540,7 @@ snapshots:
'@nodelib/fs.walk': 1.2.8
glob-parent: 5.1.2
merge2: 1.4.1
- micromatch: 4.0.7
+ micromatch: 4.0.8
fast-json-stable-stringify@2.1.0: {}
@@ -4895,7 +4903,6 @@ snapshots:
'@babel/core': 7.25.2
'@jest/test-sequencer': 29.7.0
'@jest/types': 29.6.3
- '@types/node': 22.2.0
babel-jest: 29.7.0(@babel/core@7.25.2)
chalk: 4.1.2
ci-info: 3.9.0
@@ -4910,11 +4917,13 @@ snapshots:
jest-runner: 29.7.0
jest-util: 29.7.0
jest-validate: 29.7.0
- micromatch: 4.0.7
+ micromatch: 4.0.8
parse-json: 5.2.0
pretty-format: 29.7.0
slash: 3.0.0
strip-json-comments: 3.1.1
+ optionalDependencies:
+ '@types/node': 22.2.0
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
@@ -4975,7 +4984,7 @@ snapshots:
jest-regex-util: 29.6.3
jest-util: 29.7.0
jest-worker: 29.7.0
- micromatch: 4.0.7
+ micromatch: 4.0.8
walker: 1.0.8
optionalDependencies:
fsevents: 2.3.3
@@ -4999,7 +5008,7 @@ snapshots:
'@types/stack-utils': 2.0.3
chalk: 4.1.2
graceful-fs: 4.2.11
- micromatch: 4.0.7
+ micromatch: 4.0.8
pretty-format: 29.7.0
slash: 3.0.0
stack-utils: 2.0.6
@@ -5011,7 +5020,7 @@ snapshots:
jest-util: 29.7.0
jest-pnp-resolver@1.2.3(jest-resolve@29.7.0):
- dependencies:
+ optionalDependencies:
jest-resolve: 29.7.0
jest-regex-util@29.6.3: {}
@@ -5509,7 +5518,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- micromatch@4.0.7:
+ micromatch@4.0.8:
dependencies:
braces: 3.0.3
picomatch: 2.3.1
@@ -5673,8 +5682,9 @@ snapshots:
postcss-load-config@4.0.2(postcss@8.4.41):
dependencies:
lilconfig: 3.1.2
- postcss: 8.4.41
yaml: 2.5.0
+ optionalDependencies:
+ postcss: 8.4.41
postcss-nested@6.2.0(postcss@8.4.41):
dependencies:
@@ -5738,6 +5748,8 @@ snapshots:
react: 18.3.1
scheduler: 0.23.2
+ react-ga4@2.1.0: {}
+
react-is@17.0.2: {}
react-is@18.3.1: {}
@@ -5763,22 +5775,24 @@ snapshots:
react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@18.3.1):
dependencies:
- '@types/react': 18.3.3
react: 18.3.1
react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1)
tslib: 2.6.3
+ optionalDependencies:
+ '@types/react': 18.3.3
react-remove-scroll@2.5.7(@types/react@18.3.3)(react@18.3.1):
dependencies:
- '@types/react': 18.3.3
react: 18.3.1
react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1)
react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1)
tslib: 2.6.3
use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1)
use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.3
- react-router-dom@6.26.0(react-dom@18.3.1)(react@18.3.1):
+ react-router-dom@6.26.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@remix-run/router': 1.19.0
react: 18.3.1
@@ -5792,11 +5806,12 @@ snapshots:
react-style-singleton@2.2.1(@types/react@18.3.3)(react@18.3.1):
dependencies:
- '@types/react': 18.3.3
get-nonce: 1.0.1
invariant: 2.2.4
react: 18.3.1
tslib: 2.6.3
+ optionalDependencies:
+ '@types/react': 18.3.3
react@18.3.1:
dependencies:
@@ -6015,7 +6030,7 @@ snapshots:
is-glob: 4.0.3
jiti: 1.21.6
lilconfig: 2.1.0
- micromatch: 4.0.7
+ micromatch: 4.0.8
normalize-path: 3.0.0
object-hash: 3.0.0
picocolors: 1.0.1
@@ -6075,9 +6090,8 @@ snapshots:
ts-interface-checker@0.1.13: {}
- ts-jest@29.2.5(@babel/core@7.25.2)(jest@29.7.0)(typescript@5.5.3):
+ ts-jest@29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@22.2.0))(typescript@5.5.3):
dependencies:
- '@babel/core': 7.25.2
bs-logger: 0.2.6
ejs: 3.1.10
fast-json-stable-stringify: 2.1.0
@@ -6089,6 +6103,11 @@ snapshots:
semver: 7.6.3
typescript: 5.5.3
yargs-parser: 21.1.1
+ optionalDependencies:
+ '@babel/core': 7.25.2
+ '@jest/transform': 29.7.0
+ '@jest/types': 29.6.3
+ babel-jest: 29.7.0(@babel/core@7.25.2)
tslib@2.6.3: {}
@@ -6102,9 +6121,10 @@ snapshots:
typescript-eslint@8.0.0(eslint@9.8.0)(typescript@5.5.3):
dependencies:
- '@typescript-eslint/eslint-plugin': 8.0.0(@typescript-eslint/parser@8.0.0)(eslint@9.8.0)(typescript@5.5.3)
+ '@typescript-eslint/eslint-plugin': 8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.8.0)(typescript@5.5.3))(eslint@9.8.0)(typescript@5.5.3)
'@typescript-eslint/parser': 8.0.0(eslint@9.8.0)(typescript@5.5.3)
'@typescript-eslint/utils': 8.0.0(eslint@9.8.0)(typescript@5.5.3)
+ optionalDependencies:
typescript: 5.5.3
transitivePeerDependencies:
- eslint
@@ -6166,16 +6186,18 @@ snapshots:
use-callback-ref@1.3.2(@types/react@18.3.3)(react@18.3.1):
dependencies:
- '@types/react': 18.3.3
react: 18.3.1
tslib: 2.6.3
+ optionalDependencies:
+ '@types/react': 18.3.3
use-sidecar@1.1.2(@types/react@18.3.3)(react@18.3.1):
dependencies:
- '@types/react': 18.3.3
detect-node-es: 1.1.0
react: 18.3.1
tslib: 2.6.3
+ optionalDependencies:
+ '@types/react': 18.3.3
util-deprecate@1.0.2: {}
@@ -6197,11 +6219,11 @@ snapshots:
vite@5.4.0(@types/node@22.2.0):
dependencies:
- '@types/node': 22.2.0
esbuild: 0.21.5
postcss: 8.4.41
rollup: 4.20.0
optionalDependencies:
+ '@types/node': 22.2.0
fsevents: 2.3.3
w3c-xmlserializer@4.0.0:
diff --git a/src/App.tsx b/src/App.tsx
index 4fe2061..456ff3a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,5 +1,4 @@
-import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { Routes, Route, useLocation } from 'react-router-dom';
import { Toaster } from '@/components/Toaster';
import LandingPage from './pages/LandingPage';
@@ -10,7 +9,6 @@ import AuthenticatedAdminPage from './pages/admin/AdminPage';
import AuthenticatedAdminSuggestionsPage from './pages/admin/AdminSuggestionsPage';
import './App.css';
-import Background from './components/Background';
import { useAuth } from './context/useAuth';
import UsernameBubble from './components/UsernameBubble';
import AuthenticatedAdminUsersPage from './pages/admin/AdminUsersPage';
@@ -19,49 +17,55 @@ import LeaderboardPage from './pages/Leaderboard';
import AuthenticatedModeratorPage from './pages/moderator/ModeratorPage';
import AuthenticatedModeratorSuggestionsPage from './pages/moderator/ModeratorSuggestionsPage';
import AuthenticatedModeratorQuestsPage from './pages/moderator/ModeratorQuestsPage';
-
-const queryClient = new QueryClient()
+import { useEffect } from 'react';
+import initializeGA, { trackPageView } from './analytics';
function App() {
const { user } = useAuth();
+ const location = useLocation();
+
+ useEffect(() => {
+ initializeGA();
+ }, []);
+
+ useEffect(() => {
+ trackPageView(location.pathname);
+ }, [location]);
return (
-
+ <>
-
-
- {/* public routes */}
- } />
- } />
- } />
- } />
+
+ {/* public routes */}
+ } />
+ } />
+ } />
+ } />
- {/* authorized routes */}
- {/* } /> */}
+ {/* authorized routes */}
+ {/* } /> */}
- {/* moderator routes */}
- } />
- } />
- } />
+ {/* moderator routes */}
+ } />
+ } />
+ } />
- {/* admin routes */}
- } />
- } />
- } />
- } />
+ {/* admin routes */}
+ } />
+ } />
+ } />
+ } />
- {/* user routes */}
- } />
-
-
+ {/* user routes */}
+ } />
+
{
user &&
}
-
-
+ >
)
}
diff --git a/src/analytics.ts b/src/analytics.ts
new file mode 100644
index 0000000..5867a3d
--- /dev/null
+++ b/src/analytics.ts
@@ -0,0 +1,49 @@
+import ReactGA from 'react-ga4';
+import crypto from 'crypto';
+
+// Initialize Google Analytics
+const initializeGA = () => {
+ const Tag = process.env.NODE_ENV === 'production' ? 'G-HVLY6L1CT8' : 'G-DEVY6L1CT8';
+ ReactGA.initialize(Tag);
+};
+
+export const hashUserId = (userId: string) => {
+ const hash = crypto.createHash('sha256');
+ hash.update(userId);
+ return hash.digest('hex');
+};
+
+// Function to track page views
+export const trackPageView = (path: string) => {
+ ReactGA.send({ hitType: 'pageview', page: path });
+};
+
+// Function to track events
+export const trackEvent = (category: string, action: string, label?: string, value?: number) => {
+ ReactGA.event({
+ category: category,
+ action: action,
+ label: label,
+ value: value,
+ });
+};
+
+// Custom events for login and registration
+export const trackLoginAttempt = () => trackEvent('User', 'Login Attempt');
+export const trackLoginSuccess = () => trackEvent('User', 'Login Success');
+export const trackLoginFailure = (error: string) => trackEvent('User', 'Login Failure', error);
+
+export const trackRegistrationAttempt = () => trackEvent('User', 'Registration Attempt');
+export const trackRegistrationSuccess = () => trackEvent('User', 'Registration Success');
+export const trackRegistrationFailure = (error: string) => trackEvent('User', 'Registration Failure', error);
+
+export const trackGuestLogin = () => trackEvent('User', 'Guest Login');
+
+// Custom events for quests
+export const trackQuestCompletion = (questId: number) => trackEvent('Quest', 'Complete Quest', questId.toString());
+export const trackQuestSkip = (questId: number) => trackEvent('Quest', 'Skip Quest', questId.toString());
+
+export const trackSuggestionCreation = () => trackEvent('Suggestion', 'Create Suggestion');
+
+// Export the initialization function
+export default initializeGA;
diff --git a/src/components/LoginPanel/LoginPanel.tsx b/src/components/LoginPanel/LoginPanel.tsx
index 7f84464..e8f05b0 100644
--- a/src/components/LoginPanel/LoginPanel.tsx
+++ b/src/components/LoginPanel/LoginPanel.tsx
@@ -5,7 +5,15 @@ import Button from "../Button";
import { toast } from "../Toaster";
import authService from "@/service/authService";
import { useNavigate } from "react-router-dom";
-
+import {
+ trackLoginAttempt,
+ trackLoginSuccess,
+ trackLoginFailure,
+ trackRegistrationAttempt,
+ trackRegistrationSuccess,
+ trackRegistrationFailure,
+ trackGuestLogin,
+} from '@/analytics';
interface LoginPanelProps {
onLoginSuccess: () => void;
onRegistrationSuccess: () => void;
@@ -45,17 +53,28 @@ const LoginPanel: React.FC = ({ onLoginSuccess, onRegistrationS
setDisableButtons(true);
try {
let authResponse;
+
if (isRegistering) {
+ trackRegistrationAttempt();
authResponse = await authService.register(username, password);
+ trackRegistrationSuccess();
onRegistrationSuccess();
} else {
+ trackLoginAttempt();
authResponse = await authService.login(username, password);
+ trackLoginSuccess();
onLoginSuccess();
}
saveToken(authResponse.token);
} catch (error: unknown) {
if (error instanceof AxiosError) {
+ if (isRegistering) {
+ trackRegistrationFailure(error.response?.data?.error || 'Unknown Error');
+ } else {
+ trackLoginFailure(error.response?.data?.error || 'Unknown Error');
+ }
+
if (error.response?.status === 429) {
setError("Too many requests. Please try again later.");
} else {
@@ -72,6 +91,7 @@ const LoginPanel: React.FC = ({ onLoginSuccess, onRegistrationS
const loginAsGuest = async () => {
setDisableButtons(true);
try {
+ trackGuestLogin();
const loginGuestResponse = await authService.loginGuest();
saveToken(loginGuestResponse.token);
onRegistrationSuccess();
diff --git a/src/components/SuggestionsPanel/SuggestionsPanel.tsx b/src/components/SuggestionsPanel/SuggestionsPanel.tsx
index d254f19..0753edc 100644
--- a/src/components/SuggestionsPanel/SuggestionsPanel.tsx
+++ b/src/components/SuggestionsPanel/SuggestionsPanel.tsx
@@ -6,6 +6,7 @@ import { AxiosError } from "axios";
import { useAuth } from "@/context/useAuth";
import Button from "../Button";
import { toast } from "../Toaster";
+import { trackSuggestionCreation } from "@/analytics";
type SuggestionsPanelProps = {
onClose: () => void;
@@ -37,6 +38,7 @@ const SuggestionsPanel: React.FC = (props) => {
newSuggestion.title = newSuggestion.title.trim();
newSuggestion.description = newSuggestion.description.trim();
+ trackSuggestionCreation();
await suggestionService.createSuggestion(newSuggestion, accessToken!);
toast.success("Suggestion submitted successfully!");
onClose();
diff --git a/src/main.tsx b/src/main.tsx
index 31a0603..c2c58f3 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -2,11 +2,16 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import { AuthProvider } from './context/AuthenticationProvider.tsx'
+import Background from './components/Background/Background.tsx'
+import { BrowserRouter as Router } from 'react-router-dom';
createRoot(document.getElementById('root')!).render(
-
+
+
+
+
,
)
diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx
index 3a08c92..d7b3d55 100644
--- a/src/pages/LandingPage.tsx
+++ b/src/pages/LandingPage.tsx
@@ -19,6 +19,7 @@ import RegisterAccount from "@/modals/RegisterAccount";
import { ModalTypes } from "@/models/modals";
import ConfirmDialog from "@/components/ConfirmDialog";
import SpinningWheel from "@/components/SpinningWheel";
+import { trackQuestCompletion, trackQuestSkip } from "@/analytics";
const LandingPage = () => {
@@ -75,6 +76,9 @@ const LandingPage = () => {
const handleQuestSkip = () => {
// setCurrentQuest(null);
localStorage.removeItem('currentQuest');
+ if (currentQuest) {
+ trackQuestSkip(currentQuest.id);
+ };
handleSpinWheel();
};
@@ -82,8 +86,10 @@ const LandingPage = () => {
if (!accessToken) return navigate("/login");
try {
+ if (!currentQuest) return;
setIsModalLoading(true);
- const completeQuestResponse = await userService.completeQuest(accessToken, currentQuest!.id);
+ trackQuestCompletion(currentQuest.id);
+ const completeQuestResponse = await userService.completeQuest(accessToken, currentQuest.id);
// setCurrentQuest(null);
localStorage.removeItem('currentQuest');