├── .DS_Store
├── .gitignore
├── README.md
├── README_AWS.md
├── client
├── .gitignore
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── public
│ ├── pie-chart.png
│ └── vite.svg
├── src
│ ├── App.css
│ ├── App.tsx
│ ├── assets
│ │ ├── backgrounds
│ │ │ ├── 3417764.jpg
│ │ │ ├── blue-2.jpg
│ │ │ ├── dark-blue.jpg
│ │ │ ├── dashboard.png
│ │ │ ├── fern-bw.jpg
│ │ │ ├── fern-color.jpg
│ │ │ └── large.mp4
│ │ ├── dashboard-google-analytics-sexy.png
│ │ ├── gifs
│ │ │ ├── Animation.gif
│ │ │ ├── Animation2.gif
│ │ │ ├── heatmap1.gif
│ │ │ ├── playground1.gif
│ │ │ └── realhomepage.gif
│ │ ├── icons
│ │ │ ├── S Analytics copy.png
│ │ │ ├── aiImage.png
│ │ │ ├── blackGlobe.png
│ │ │ ├── chart.png
│ │ │ ├── clickImage.png
│ │ │ ├── dashboard.png
│ │ │ ├── globe.png
│ │ │ ├── graphs.png
│ │ │ └── pie-chart.png
│ │ └── team
│ │ │ ├── david.png
│ │ │ ├── eric.png
│ │ │ ├── peter.png
│ │ │ └── saw.png
│ ├── components
│ │ ├── Animations
│ │ │ ├── Animations.module.css
│ │ │ └── BarAnimation.tsx
│ │ ├── ChartPages
│ │ │ ├── AllUserData.tsx
│ │ │ ├── ChartDownload.tsx
│ │ │ ├── Charts.module.css
│ │ │ ├── Charts
│ │ │ │ ├── BarGraph-clicks.tsx
│ │ │ │ ├── BarGraph-referrer.tsx
│ │ │ │ ├── DuelPieChart-clicks.tsx
│ │ │ │ ├── Heatmap.tsx
│ │ │ │ ├── LineGraph-clicks.tsx
│ │ │ │ ├── RadarGraph-clicks.tsx
│ │ │ │ ├── ScatterChart-clicks.tsx
│ │ │ │ ├── ScreenshotComponent.tsx
│ │ │ │ ├── StackedBarGraph-clicks.tsx
│ │ │ │ └── aiResponse.tsx
│ │ │ ├── SelectWebsiteDropdown.tsx
│ │ │ ├── TimeFrameDropdown.tsx
│ │ │ └── WebsiteData.tsx
│ │ ├── Documentation
│ │ │ ├── Documentation.module.css
│ │ │ └── Documentation.tsx
│ │ ├── Footer
│ │ │ ├── Footer.module.css
│ │ │ └── Footer.tsx
│ │ ├── Landing
│ │ │ ├── DashboardGif.tsx
│ │ │ ├── FAQSection.tsx
│ │ │ ├── Footer.tsx
│ │ │ ├── GettingStarted.tsx
│ │ │ ├── Hero.module.css
│ │ │ ├── Hero.tsx
│ │ │ ├── LandingView.tsx
│ │ │ ├── WhyOSA.module.css
│ │ │ ├── WhyOSA.tsx
│ │ │ └── githubProfiles.tsx
│ │ ├── Loading
│ │ │ ├── Loading.module.css
│ │ │ └── Loading.tsx
│ │ ├── Login
│ │ │ ├── Login.module.css
│ │ │ ├── Login.tsx
│ │ │ └── Signup.tsx
│ │ ├── Navbar
│ │ │ ├── NavMobile.module.css
│ │ │ ├── NavMobile.tsx
│ │ │ ├── Navbar.module.css
│ │ │ ├── Navbar.tsx
│ │ │ └── NavbarDashboard.tsx
│ │ ├── PageNotFound
│ │ │ ├── PageNotFound.module.css
│ │ │ └── PageNotFound.tsx
│ │ ├── Playground
│ │ │ ├── FrequencyReactFlow
│ │ │ │ └── FlowPlayground.tsx
│ │ │ ├── OverallReactFlow
│ │ │ │ ├── Layout.tsx
│ │ │ │ └── OverallFlowPlayground.tsx
│ │ │ ├── ViewDrawer.tsx
│ │ │ └── playgroundDisplay.tsx
│ │ ├── Settings
│ │ │ ├── ApiKeyDisplay.tsx
│ │ │ ├── ApiKeyFormat.tsx
│ │ │ ├── AwsBedrockConfig.tsx
│ │ │ ├── Dialogs.tsx
│ │ │ ├── Settings.tsx
│ │ │ ├── UseApiKey.tsx
│ │ │ └── WebsiteSelection.tsx
│ │ └── User
│ │ │ ├── ClickLog.module.css
│ │ │ ├── ClickLog.tsx
│ │ │ ├── ClickLogItem.tsx
│ │ │ ├── Dashboard.tsx
│ │ │ ├── DashboardDisplay.tsx
│ │ │ ├── ForgotPassword.tsx
│ │ │ ├── Sidebar.tsx
│ │ │ ├── UserView.module.css
│ │ │ └── UserView.tsx
│ ├── main.css
│ ├── main.tsx
│ ├── services
│ │ ├── apiKeyConfig.tsx
│ │ ├── authConfig.ts
│ │ ├── deleteDataApi.ts
│ │ ├── extractData.ts
│ │ ├── filterDataByReferralTimeFrame.tsx
│ │ ├── filterDataByTimeFrame .ts
│ │ └── populateAtoms.ts
│ ├── state
│ │ └── Atoms.tsx
│ ├── types-packages
│ │ ├── three.d.ts
│ │ └── vanta.d.ts
│ └── vite-env.d.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── types.tsx
└── vite.config.ts
├── package-lock.json
├── package.json
└── server
├── .gitignore
├── README.md
├── controllers
├── aiController.ts
├── clickController.ts
├── dataController.ts
├── oauthRequestRoutes.ts
├── oauthRoutes.ts
├── puppeteerController.ts
└── userController.ts
├── middleware
├── addToDB.ts
├── auth.ts
├── awsEncryption.ts
├── passportUserMiddleware.ts
├── verifier.ts
└── writeToDbAuth.ts
├── models
└── db.ts
├── package-lock.json
├── package.json
├── routes
├── aiRoutes.ts
├── authRoute.ts
├── clickRoutes.ts
├── dataRoute.ts
├── puppeteerRoutes.ts
└── userRoutes.ts
├── server.ts
└── types.ts
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
--------------------------------------------------------------------------------
/README_AWS.md:
--------------------------------------------------------------------------------
1 |
2 | # AWS Bedrock Setup Guide
3 |
4 | This guide will help you configure your AWS credentials and IAM permissions to use AWS Bedrock with this application.
5 |
6 | ## Step 1: Set Up Your IAM User for AWS Bedrock
7 |
8 | 1. **Log in to the AWS Management Console** by visiting [https://aws.amazon.com/console/](https://aws.amazon.com/console/).
9 | 2. In the console, navigate to **IAM** from the services menu.
10 | 3. Under **Users**, click **Add user**.
11 | 4. **Name the user** (e.g., `bedrock-user`).
12 | 5. Click **Next: Permissions**, then choose **Attach policies directly**.
13 | 6. Search for and select **AmazonBedrockFullAccess** from the list of policies.
14 | 7. Press **Next**, review your settings, and then click **Create user**.
15 |
16 | Once the user is created:
17 |
18 | 1. Go back to the **Users** section and find the user you just created (`bedrock-user`).
19 | 2. Click on the user, then go to the **Access key** link.
20 | 3. Press **Create access key**.
21 | 4. **Name your key** (for your reference) and optionally download it as a **CSV file** for safekeeping.
22 | 5. Copy your **Access Key ID** and **Secret Access Key**.
23 |
24 |
25 | ## Step 2: Enter AWS Credentials in the Application
26 |
27 | 1. Log in to the application and navigate to the **Settings** page.
28 | 2. Locate the fields for **Client Access Key**, **Secret Key**, and **Region**.
29 | 3. Enter the following details:
30 | - **Client Access Key**: Your AWS Access Key ID from Step 1.
31 | - **Secret Key**: Your AWS Secret Access Key from Step 1.
32 | - **Region**: Your region (e.g., `us-east-1`).
33 | 4. Click **Save** to store your credentials.
34 |
35 | Once the credentials are saved, you may then navigate to dashboard and use the AWS bedrock feature!
36 |
37 | ### Securing Your Credentials
38 | Your AWS credentials are securely stored in our database using encryption, ensuring they remain protected and accessible only to you.
39 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_STORE
2 | node_modules
3 | scripts/flow/*/.flowconfig
4 | .flowconfig
5 | *~
6 | *.pyc
7 | .grunt
8 | _SpecRunner.html
9 | __benchmarks__
10 | build/
11 | remote-repo/
12 | coverage/
13 | .module-cache
14 | fixtures/dom/public/react-dom.js
15 | fixtures/dom/public/react.js
16 | test/the-files-to-test.generated.js
17 | *.log*
18 | chrome-user-data
19 | *.sublime-project
20 | *.sublime-workspace
21 | .idea
22 | *.iml
23 | .vscode
24 | *.swp
25 | *.swo
26 |
27 | packages/react-devtools-core/dist
28 | packages/react-devtools-extensions/chrome/build
29 | packages/react-devtools-extensions/chrome/*.crx
30 | packages/react-devtools-extensions/chrome/*.pem
31 | packages/react-devtools-extensions/firefox/build
32 | packages/react-devtools-extensions/firefox/*.xpi
33 | packages/react-devtools-extensions/firefox/*.pem
34 | packages/react-devtools-extensions/shared/build
35 | packages/react-devtools-extensions/.tempUserDataDir
36 | packages/react-devtools-fusebox/dist
37 | packages/react-devtools-inline/dist
38 | packages/react-devtools-shell/dist
39 | packages/react-devtools-timeline/dist
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | export default tseslint.config({
18 | languageOptions: {
19 | // other options...
20 | parserOptions: {
21 | project: ['./tsconfig.node.json', './tsconfig.app.json'],
22 | tsconfigRootDir: import.meta.dirname,
23 | },
24 | },
25 | })
26 | ```
27 |
28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
29 | - Optionally add `...tseslint.configs.stylisticTypeChecked`
30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
31 |
32 | ```js
33 | // eslint.config.js
34 | import react from 'eslint-plugin-react'
35 |
36 | export default tseslint.config({
37 | // Set the react version
38 | settings: { react: { version: '18.3' } },
39 | plugins: {
40 | // Add the react plugin
41 | react,
42 | },
43 | rules: {
44 | // other rules...
45 | // Enable its recommended rules
46 | ...react.configs.recommended.rules,
47 | ...react.configs['jsx-runtime'].rules,
48 | },
49 | })
50 | ```
51 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | OS Analytics - Comprehensive Analytic Tools For Developers
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "activetytracker-io-frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build --watch",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@emotion/react": "^11.13.3",
14 | "@emotion/styled": "^11.13.0",
15 | "@mui/icons-material": "^6.0.1",
16 | "@mui/material": "^6.0.2",
17 | "@mui/styled-engine-sc": "^6.0.2",
18 | "@xyflow/react": "^12.1.1",
19 | "axios": "^1.7.4",
20 | "chart.js": "^4.4.4",
21 | "dagre": "^0.8.5",
22 | "framer-motion": "^11.3.31",
23 | "heatmap.js": "https://github.com/ionata/heatmap.js.git",
24 | "html-to-image": "^1.11.11",
25 | "html2canvas": "^1.4.1",
26 | "jotai": "^2.9.3",
27 | "jspdf": "^2.5.1",
28 | "passport-google-oauth": "^2.0.0",
29 | "puppeteer": "^23.3.0",
30 | "react": "^18.3.1",
31 | "react-chartjs-2": "^5.2.0",
32 | "react-copy-to-clipboard": "^5.1.0",
33 | "react-dom": "^18.3.1",
34 | "react-flow-renderer": "^10.3.17",
35 | "react-router-dom": "^6.26.1",
36 | "react-syntax-highlighter": "^15.5.0",
37 | "styled-components": "^6.1.13",
38 | "three": "^0.134.0",
39 | "uuid": "^10.0.0",
40 | "vanta": "^0.5.24"
41 | },
42 | "devDependencies": {
43 | "@eslint/js": "^9.9.0",
44 | "@types/d3": "^7.4.3",
45 | "@types/dagre": "^0.7.52",
46 | "@types/heatmap.js": "^2.0.41",
47 | "@types/node": "^22.5.4",
48 | "@types/puppeteer-core": "^7.0.4",
49 | "@types/react": "^18.3.3",
50 | "@types/react-copy-to-clipboard": "^5.0.7",
51 | "@types/react-dom": "^18.3.0",
52 | "@types/react-syntax-highlighter": "^15.5.13",
53 | "@types/three": "^0.168.0",
54 | "@vitejs/plugin-react": "^4.3.1",
55 | "eslint": "^9.9.0",
56 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
57 | "eslint-plugin-react-refresh": "^0.4.9",
58 | "globals": "^15.9.0",
59 | "typescript": "^5.5.4",
60 | "typescript-eslint": "^8.0.1",
61 | "vite": "^5.4.1"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/client/public/pie-chart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/public/pie-chart.png
--------------------------------------------------------------------------------
/client/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/App.css:
--------------------------------------------------------------------------------
1 | .app {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | width: 100vw;
6 | max-width: 1200px;
7 | height: 100vh;
8 | overflow-y: hidden;
9 | }
--------------------------------------------------------------------------------
/client/src/App.tsx:
--------------------------------------------------------------------------------
1 | import LandingView from './components/Landing/LandingView';
2 | import Login from './components/Login/Login';
3 | import UserView from './components/User/UserView';
4 | import PageNotFound from './components/PageNotFound/PageNotFound';
5 | import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
6 | import { activeUserAtom } from './state/Atoms';
7 | import { useAtom } from 'jotai';
8 | import { useEffect, useState } from 'react';
9 | import { handleSession } from "./services/authConfig";
10 | import Signup from './components/Login/Signup';
11 | import Documentation from './components/Documentation/Documentation';
12 | import Settings from './components/Settings/Settings';
13 | import ForgotPassword from './components/User/ForgotPassword';
14 | import PlaygroundDisplay from './components/Playground/playgroundDisplay';
15 |
16 |
17 | function App() {
18 | const [activeUser, setActiveUser] = useAtom(activeUserAtom); //email of active user || null
19 | const [loading, setLoading] = useState(true);
20 |
21 | useEffect(() => {
22 | const fetchUser = async () => {
23 | const user = await handleSession();
24 | if (user) {
25 | setActiveUser(user);
26 | }
27 | setLoading(false);
28 | };
29 |
30 | fetchUser();
31 | }, []);
32 |
33 | if (loading) return;
34 |
35 | return (
36 |
37 |
38 | } />
39 | : }
42 | />
43 | : } />
44 | : } />
45 | : } />
46 | } />
47 | : } />
48 | : } />
49 | } />
50 | } />
51 |
52 |
53 | );
54 | }
55 |
56 | export default App;
57 |
--------------------------------------------------------------------------------
/client/src/assets/backgrounds/3417764.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/backgrounds/3417764.jpg
--------------------------------------------------------------------------------
/client/src/assets/backgrounds/blue-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/backgrounds/blue-2.jpg
--------------------------------------------------------------------------------
/client/src/assets/backgrounds/dark-blue.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/backgrounds/dark-blue.jpg
--------------------------------------------------------------------------------
/client/src/assets/backgrounds/dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/backgrounds/dashboard.png
--------------------------------------------------------------------------------
/client/src/assets/backgrounds/fern-bw.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/backgrounds/fern-bw.jpg
--------------------------------------------------------------------------------
/client/src/assets/backgrounds/fern-color.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/backgrounds/fern-color.jpg
--------------------------------------------------------------------------------
/client/src/assets/backgrounds/large.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/backgrounds/large.mp4
--------------------------------------------------------------------------------
/client/src/assets/dashboard-google-analytics-sexy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/dashboard-google-analytics-sexy.png
--------------------------------------------------------------------------------
/client/src/assets/gifs/Animation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/gifs/Animation.gif
--------------------------------------------------------------------------------
/client/src/assets/gifs/Animation2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/gifs/Animation2.gif
--------------------------------------------------------------------------------
/client/src/assets/gifs/heatmap1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/gifs/heatmap1.gif
--------------------------------------------------------------------------------
/client/src/assets/gifs/playground1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/gifs/playground1.gif
--------------------------------------------------------------------------------
/client/src/assets/gifs/realhomepage.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/gifs/realhomepage.gif
--------------------------------------------------------------------------------
/client/src/assets/icons/S Analytics copy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/icons/S Analytics copy.png
--------------------------------------------------------------------------------
/client/src/assets/icons/aiImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/icons/aiImage.png
--------------------------------------------------------------------------------
/client/src/assets/icons/blackGlobe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/icons/blackGlobe.png
--------------------------------------------------------------------------------
/client/src/assets/icons/chart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/icons/chart.png
--------------------------------------------------------------------------------
/client/src/assets/icons/clickImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/icons/clickImage.png
--------------------------------------------------------------------------------
/client/src/assets/icons/dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/icons/dashboard.png
--------------------------------------------------------------------------------
/client/src/assets/icons/globe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/icons/globe.png
--------------------------------------------------------------------------------
/client/src/assets/icons/graphs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/icons/graphs.png
--------------------------------------------------------------------------------
/client/src/assets/icons/pie-chart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/icons/pie-chart.png
--------------------------------------------------------------------------------
/client/src/assets/team/david.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/team/david.png
--------------------------------------------------------------------------------
/client/src/assets/team/eric.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/team/eric.png
--------------------------------------------------------------------------------
/client/src/assets/team/peter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/team/peter.png
--------------------------------------------------------------------------------
/client/src/assets/team/saw.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/OS-Analytics/685421537ea8a18b3f896a8a85edfdbd0e2cdfcc/client/src/assets/team/saw.png
--------------------------------------------------------------------------------
/client/src/components/Animations/Animations.module.css:
--------------------------------------------------------------------------------
1 | .barContainer {
2 | width: 92px;
3 | height: 70px;
4 | border-left: 3px solid white;
5 | border-bottom: 3px solid white;
6 | background: transparent;
7 | display: flex;
8 | gap: 5%;
9 | justify-content: center;
10 | align-items: end;
11 | }
12 |
13 | .bar {
14 | width: 25%;
15 | height: 20%;
16 | border-top-left-radius: 4px;
17 | border-top-right-radius: 4px;
18 | background: coral;
19 | transition: all .5s ease-in-out;
20 | }
21 |
22 | #red {
23 | background: #f67259;
24 | }
25 | #blue {
26 | background: #3aabe0;
27 | }
28 |
29 | #yellow {
30 | background: #fbdc65;
31 | }
--------------------------------------------------------------------------------
/client/src/components/Animations/BarAnimation.tsx:
--------------------------------------------------------------------------------
1 | import styles from './Animations.module.css';
2 |
3 | export default function BarAnimation() {
4 |
5 |
6 | function getHeight() {
7 | return Math.floor(Math.random()*80)+20;
8 | }
9 |
10 |
11 | return (
12 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/client/src/components/ChartPages/AllUserData.tsx:
--------------------------------------------------------------------------------
1 | import { Grid, Paper, Box } from "@mui/material";
2 | import ClickGraph from "./Charts/LineGraph-clicks";
3 | import DuelPieGraphs from "./Charts/DuelPieChart-clicks";
4 | import BarGraph from "./Charts/BarGraph-clicks";
5 | import AiResponseComponent from "./Charts/aiResponse";
6 | import { userDataAtom } from "../../state/Atoms";
7 | import { useAtom } from "jotai";
8 | import { mapUserData } from "../../services/extractData";
9 | import RadarChart from "./Charts/RadarGraph-clicks";
10 | import StackedBarChart from "./Charts/StackedBarGraph-clicks";
11 |
12 | const AllUserData = () => {
13 | const [userData] = useAtom(userDataAtom);
14 | const mappedData = mapUserData(userData);
15 |
16 | return (
17 |
18 |
19 |
20 |
30 |
31 |
32 |
33 |
34 |
35 |
46 |
51 |
52 |
53 |
54 |
55 |
65 |
70 |
71 |
72 |
73 |
83 |
88 |
89 |
90 |
91 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | );
128 | };
129 |
130 | export default AllUserData;
131 |
--------------------------------------------------------------------------------
/client/src/components/ChartPages/ChartDownload.tsx:
--------------------------------------------------------------------------------
1 | import { IconButton } from '@mui/material';
2 | import DownloadIcon from '@mui/icons-material/Download';
3 | import { ChartDownloadProps } from '../../../types';
4 | const ChartDownload = ({ chartRef, fileName = 'chart.png' }: ChartDownloadProps) => {
5 | const handleDownload = () => {
6 | if (chartRef.current) {
7 | const chartInstance = chartRef.current;
8 | const link = document.createElement('a');
9 | link.href = chartInstance.toBase64Image();
10 | link.download = fileName;
11 | link.click();
12 | }
13 | };
14 |
15 | return (
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default ChartDownload;
23 |
--------------------------------------------------------------------------------
/client/src/components/ChartPages/Charts.module.css:
--------------------------------------------------------------------------------
1 |
2 | .chartDisplay {
3 | display: flex;
4 | gap: 2rem;
5 | flex-wrap:wrap;
6 | justify-content:center;
7 | align-items: center;
8 |
9 | /* display: grid;
10 | grid-template-columns: 1fr 1fr;
11 | gap: 1rem;
12 | color: var(--black);
13 | width: calc(100vw - 7rem);
14 | max-width: calc(100vw - 4rem);
15 | background: rgba(255, 127, 80, 0.338);
16 | margin: 0 1rem; */
17 | }
18 | b
19 | .chartBox {
20 | background: rgba(148, 148, 148, 0.044);
21 | display: grid;
22 | place-content: center;
23 | border: 1px solid var(--blue-secondary);
24 | border-radius: 2px;
25 | width: 300px;
26 | max-width: calc(100vw - 6rem);
27 | }
28 | /* .chartBox:nth-child(1), .chartBox:nth-child(4), .chartBox:nth-child(7), .chartBox:nth-child(10) {
29 | grid-column: span 2;
30 | background: transparent;
31 | } */
32 | @media only screen and (max-width: 1000px) {
33 | .chartDisplay {
34 | display: flex;
35 | flex-direction: column;
36 |
37 | }
38 | }
--------------------------------------------------------------------------------
/client/src/components/ChartPages/Charts/BarGraph-clicks.tsx:
--------------------------------------------------------------------------------
1 | import { Bar } from "react-chartjs-2";
2 | import {
3 | Chart as ChartJS,
4 | CategoryScale,
5 | LinearScale,
6 | BarElement,
7 | Title,
8 | Tooltip,
9 | Legend,
10 | } from "chart.js";
11 | import { useAtom } from "jotai";
12 | import { timeFrameAtom } from '../../../state/Atoms';
13 | import styles from '../Charts.module.css'
14 | import { filterDataByTimeFrame } from "../../../services/filterDataByTimeFrame ";
15 | import { BarChartProps } from "../../../../types"
16 | import ChartDownload from "../ChartDownload";
17 | import { useRef } from "react";
18 |
19 | ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
20 |
21 | const BarChart = ({ data, keyword }: BarChartProps) => {
22 | const chartRef = useRef(null);
23 | const [timeFrame] = useAtom(timeFrameAtom);
24 |
25 | const filteredData = filterDataByTimeFrame(data, timeFrame);
26 | const websiteCounts: { [key: string]: number } = {};
27 |
28 | filteredData.forEach(item => {
29 | if (websiteCounts[item[keyword]]) {
30 | websiteCounts[item[keyword]] += 1;
31 | } else {
32 | websiteCounts[item[keyword]] = 1;
33 | }
34 | });
35 |
36 | const labels = Object.keys(websiteCounts);
37 | const counts = Object.values(websiteCounts);
38 |
39 | const colorPattern = [
40 | "rgba(54, 162, 235, 0.8)",
41 | "rgba(255, 99, 132, 0.8)",
42 | ];
43 |
44 | const backgroundColors = labels.map((_, index) => colorPattern[index % colorPattern.length]);
45 | const borderColors = labels.map((_, index) => colorPattern[index % colorPattern.length].replace("0.8", "1"));
46 |
47 | const chartData = {
48 | labels: labels,
49 | datasets: [
50 | {
51 | label: "Clicks",
52 | data: counts,
53 | backgroundColor: backgroundColors,
54 | borderColor: borderColors,
55 | borderWidth: 3,
56 | borderRadius: 5,
57 | },
58 | ],
59 | };
60 |
61 | const options = {
62 | scales: {
63 | y: {
64 | beginAtZero: true,
65 | ticks: {
66 | stepSize: 1,
67 | },
68 | },
69 | },
70 | plugins: {
71 | legend: {
72 | display: false,
73 | },
74 | },
75 | };
76 |
77 | return (
78 |
79 |
Clicks per website
80 |
81 |
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default BarChart;
89 |
--------------------------------------------------------------------------------
/client/src/components/ChartPages/Charts/BarGraph-referrer.tsx:
--------------------------------------------------------------------------------
1 | import { Bar } from "react-chartjs-2";
2 | import {
3 | Chart as ChartJS,
4 | CategoryScale,
5 | LinearScale,
6 | BarElement,
7 | Title,
8 | Tooltip,
9 | Legend,
10 | ChartOptions,
11 | } from "chart.js";
12 | import { useAtom } from "jotai";
13 | import { timeFrameAtom } from '../../../state/Atoms';
14 | import styles from '../Charts.module.css';
15 | import { filterReferralDataByTimeFrame } from "../../../services/filterDataByReferralTimeFrame";
16 | import { referralBarChartProps } from "../../../../types";
17 | import ChartDownload from "../ChartDownload";
18 | import { useRef } from "react";
19 | ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
20 |
21 | const BarChart = ({ data }: referralBarChartProps) => {
22 | const chartRef = useRef(null);
23 | const [timeFrame] = useAtom(timeFrameAtom);
24 | const filteredData = filterReferralDataByTimeFrame(data, timeFrame);
25 | const websiteCounts: { [key: string]: number } = {};
26 |
27 | filteredData.forEach(item => {
28 | if (websiteCounts[item.referrer]) {
29 | websiteCounts[item.referrer] += 1;
30 | } else {
31 | websiteCounts[item.referrer] = 1;
32 | }
33 | });
34 |
35 |
36 | const sortedReferrals = Object.entries(websiteCounts)
37 | .sort(([, countA], [, countB]) => countB - countA)
38 | .slice(0, 8);
39 |
40 |
41 | const labels = sortedReferrals.map(([referrer]) => referrer);
42 | const counts = sortedReferrals.map(([, count]) => count);
43 |
44 | const colorPattern = [
45 | "rgba(54, 162, 235, 0.8)",
46 | "rgba(255, 99, 132, 0.8)",
47 | ];
48 |
49 | const backgroundColors = labels.map((_, index) => colorPattern[index % colorPattern.length]);
50 | const borderColors = labels.map((_, index) => colorPattern[index % colorPattern.length].replace("0.8", "1"));
51 |
52 | const chartData = {
53 | labels: labels,
54 | datasets: [
55 | {
56 | label: "Referrals",
57 | data: counts,
58 | backgroundColor: backgroundColors,
59 | borderColor: borderColors,
60 | borderWidth: 3,
61 | borderRadius: 5,
62 | },
63 | ],
64 | };
65 |
66 | const options: ChartOptions<'bar'>= {
67 | indexAxis: 'y',
68 | scales: {
69 | x: {
70 | beginAtZero: true,
71 | ticks: {
72 | stepSize: 1,
73 | },
74 | },
75 | },
76 | plugins: {
77 | legend: {
78 | display: false,
79 | },
80 | title: {
81 | display: true,
82 | text: "Top referrals",
83 | color: 'white',
84 | font: {
85 | size: 20
86 | }
87 | },
88 | },
89 | };
90 |
91 | return (
92 |
93 |
94 |
95 |
96 |
Referred by
97 |
98 |
99 |
100 |
101 |
102 |
103 | );
104 | };
105 |
106 | export default BarChart;
107 |
--------------------------------------------------------------------------------
/client/src/components/ChartPages/Charts/DuelPieChart-clicks.tsx:
--------------------------------------------------------------------------------
1 | import { Pie, Doughnut } from "react-chartjs-2";
2 | import { useAtom } from "jotai";
3 | import { timeFrameAtom } from "../../../state/Atoms";
4 | import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
5 | import styles from "../Charts.module.css";
6 | import { filterDataByTimeFrame } from "../../../services/filterDataByTimeFrame ";
7 | import { PieChartsProps } from "../../../../types";
8 | import ChartDownload from "../ChartDownload";
9 | import { useRef } from "react";
10 |
11 | ChartJS.register(ArcElement, Tooltip, Legend);
12 |
13 | const PieCharts = ({ data, keyword, keywordTwo }: PieChartsProps) => {
14 | const [timeFrame] = useAtom(timeFrameAtom);
15 | const browserChartRef = useRef(null);
16 | const osChartRef = useRef(null);
17 | const filteredData = filterDataByTimeFrame(data, timeFrame);
18 | const browserCounts: { [key: string]: number } = {};
19 | const osCounts: { [key: string]: number } = {};
20 |
21 | filteredData.forEach((item) => {
22 | browserCounts[item[keyword]] = (browserCounts[item[keyword]] || 0) + 1;
23 | osCounts[item[keywordTwo]] = (osCounts[item[keywordTwo]] || 0) + 1;
24 | });
25 |
26 | const browserLabels = Object.keys(browserCounts);
27 | const browserData = Object.values(browserCounts);
28 |
29 | const osLabels = Object.keys(osCounts);
30 | const osData = Object.values(osCounts);
31 |
32 | const browserChartData = {
33 | labels: browserLabels,
34 | datasets: [
35 | {
36 | label: "Browsers",
37 | data: browserData,
38 | backgroundColor: [
39 | "rgba(0, 122, 255, 0.3)",
40 | "rgba(251, 188, 5, 0.3)",
41 | "rgba(0, 82, 204, 0.3)",
42 | "rgba(255, 69, 0, 0.3)",
43 | ],
44 | borderColor: [
45 | "rgba(0, 122, 255, 1)",
46 | "rgba(251, 188, 5,1",
47 | "rgba(0, 82, 204, 1)",
48 | "rgba(255, 69, 0, 1)",
49 | ],
50 | borderWidth: 1,
51 | },
52 | ],
53 | };
54 |
55 | const osChartData = {
56 | labels: osLabels,
57 | datasets: [
58 | {
59 | label: "Operating Systems",
60 | data: osData,
61 | backgroundColor: [
62 | "rgba(0, 122, 255, 0.3)",
63 | "rgba(251, 188, 5, 0.3)",
64 | "rgba(0, 82, 204, 0.3)",
65 | "rgba(255, 69, 0, 0.3)",
66 | ],
67 | borderColor: [
68 | "rgba(0, 122, 255, 1)",
69 | "rgba(251, 188, 5, 1)",
70 | "rgba(0, 82, 204, 1)",
71 | "rgba(255, 69, 0, 1)",
72 | ],
73 | borderWidth: 1,
74 | },
75 | ],
76 | };
77 |
78 | return (
79 |
88 |
89 |
92 | User Browsers
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
104 | User Operating Systems
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | );
113 | };
114 |
115 | export default PieCharts;
116 |
--------------------------------------------------------------------------------
/client/src/components/ChartPages/Charts/Heatmap.tsx:
--------------------------------------------------------------------------------
1 | import h337 from "heatmap.js";
2 | import { useState, useEffect } from "react";
3 | import { NoKeywordChart } from "../../../../types";
4 | import ScreenshotComponent from './ScreenshotComponent.tsx';
5 |
6 |
7 | /*******HEATMAP********/
8 | const Heatmap = ({ data }: NoKeywordChart) => {
9 | // length and width of viewport
10 | const screenHeight:number = 720;
11 | const screenWidth:number = 1280;
12 | const [pageUrl, setPageUrl] = useState(""); // page we are looking at
13 |
14 | // rerender on data change
15 | useEffect(() => {
16 | // grab x,y coords from data
17 | const heatData: {'x':number, 'y': number, 'value': number}[] = data.map(point => ({
18 | "x":Math.round(point.x_coord * screenWidth),
19 | "y":Math.round(point.y_coord * screenHeight),
20 | "value": 1,
21 | "radius": 10
22 | }));
23 |
24 | // check if heatmap already exists, if yes, remove existing heatmap
25 | if (document.querySelector('.heatmap-canvas')) {document.querySelectorAll('.heatmap-canvas')?.forEach(e => e.remove());}
26 | // the element we are looking for
27 | const container = document.querySelector('.heatmapContainer');
28 | // create heatmap
29 | const heatmapInstance = h337.create({
30 | container: container as HTMLElement,
31 | })
32 |
33 | const points: {min: number, max:number, data: {'x':number, 'y': number, 'value': number}[]} = {
34 | min: 1,
35 | max: 1,
36 | data: heatData
37 | };
38 |
39 | heatmapInstance.setData(points);
40 | if (data.length > 0) setPageUrl(data['0']['page_url'])
41 | }, [data])
42 |
43 | return (
44 |
45 |
Website Heatmap
46 |
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | export default Heatmap;
--------------------------------------------------------------------------------
/client/src/components/ChartPages/Charts/LineGraph-clicks.tsx:
--------------------------------------------------------------------------------
1 | import { Line } from "react-chartjs-2";
2 | import { useAtom } from "jotai";
3 | import { timeFrameAtom } from "../../../state/Atoms";
4 | import {
5 | Chart as ChartJS,
6 | CategoryScale,
7 | LinearScale,
8 | PointElement,
9 | LineElement,
10 | Title,
11 | Tooltip,
12 | Legend,
13 | Filler,
14 | } from "chart.js";
15 | import { useRef } from "react";
16 | import { filterDataByTimeFrame } from "../../../services/filterDataByTimeFrame ";
17 | import { NoKeywordChart } from "../../../../types";
18 | import ChartDownload from "../ChartDownload";
19 | ChartJS.register(
20 | CategoryScale,
21 | LinearScale,
22 | PointElement,
23 | LineElement,
24 | Title,
25 | Tooltip,
26 | Legend,
27 | Filler
28 | );
29 |
30 | const ClickGraph = ({ data }: NoKeywordChart) => {
31 | const [timeFrame] = useAtom(timeFrameAtom);
32 | const filteredData = filterDataByTimeFrame(data, timeFrame);
33 | const chartRef = useRef(null);
34 | const aggregatedData: { [key: string]: number } = {};
35 |
36 | const formatDate = (date: Date, options: Intl.DateTimeFormatOptions) =>
37 | date.toLocaleDateString("en-US", options);
38 |
39 | for (let i = 0; i < filteredData.length; i++) {
40 | const curr = filteredData[i];
41 | const clickTime = new Date(curr.time || 0);
42 |
43 | let timeKey = "";
44 |
45 | switch (timeFrame) {
46 | case "1 day":
47 | timeKey = `${clickTime.getHours()}:00`;
48 | break;
49 | case "1 week":
50 | timeKey = formatDate(clickTime, { weekday: "short" });
51 | break;
52 | case "1 month":
53 | timeKey = formatDate(clickTime, { month: "short", day: "numeric" });
54 | break;
55 | case "1 year":
56 | timeKey = `${clickTime.getMonth() + 1}/${clickTime.getFullYear()}`;
57 | break;
58 | case "5 years":
59 | timeKey = `${clickTime.getFullYear()}`;
60 | break;
61 | case "allTime":
62 | timeKey = formatDate(clickTime, { month: "short", year: "numeric" });
63 | break;
64 | default:
65 | timeKey = clickTime.toISOString();
66 | break;
67 | }
68 |
69 | if (aggregatedData[timeKey]) {
70 | aggregatedData[timeKey] += 1;
71 | } else {
72 | aggregatedData[timeKey] = 1;
73 | }
74 | }
75 |
76 | const chartData = {
77 |
78 | labels: Object.keys(aggregatedData),
79 | datasets: [
80 | {
81 | label: `Clicks over the selected timeframe`,
82 | data: Object.values(aggregatedData),
83 | fill: true,
84 | backgroundColor: "rgba(75,192,192,0.4)",
85 | borderColor: "rgba(75,192,192,1)",
86 | tension: 0.1,
87 | },
88 | ],
89 | };
90 |
91 | const chartOptions = {
92 |
93 | scales: {
94 | x: {
95 | ticks: {
96 | maxTicksLimit: timeFrame === "allTime" || "1 month" ? 7 : undefined,
97 | autoSkip: true,
98 | },
99 | },
100 | y: {
101 | beginAtZero: true,
102 | },
103 | },
104 | };
105 |
106 | return (
107 |
110 |
Click data overview
111 |
112 |
113 |
114 |
115 |
116 | );
117 | };
118 |
119 | export default ClickGraph;
120 |
--------------------------------------------------------------------------------
/client/src/components/ChartPages/Charts/RadarGraph-clicks.tsx:
--------------------------------------------------------------------------------
1 | import { Radar } from "react-chartjs-2";
2 | import { useAtom } from "jotai";
3 | import { timeFrameAtom } from '../../../state/Atoms';
4 | import {
5 | Chart as ChartJS,
6 | RadarController,
7 | Tooltip,
8 | Legend,
9 | RadialLinearScale,
10 | } from "chart.js";
11 | import styles from '../Charts.module.css';
12 | import { filterDataByTimeFrame } from "../../../services/filterDataByTimeFrame ";
13 | import { RadarChartProps } from "../../../../types";
14 | import ChartDownload from "../ChartDownload";
15 | import { useRef } from "react";
16 | ChartJS.register(RadarController, Tooltip, Legend, RadialLinearScale);
17 |
18 | const RadarChart = ({ data, keyword, keywordTwo }: RadarChartProps) => {
19 | const [timeFrame] = useAtom(timeFrameAtom);
20 | const chartRef = useRef(null);
21 |
22 | const browserLabels = ["Chrome", "Firefox", "Safari", "Edge"];
23 |
24 | const filteredData = filterDataByTimeFrame(data, timeFrame);
25 |
26 |
27 | const websiteBrowserCounts: { [website: string]: { [browser: string]: number } } = {};
28 |
29 | filteredData.forEach(item => {
30 | const website = item[keyword];
31 | const browser = item[keywordTwo];
32 |
33 | if (!websiteBrowserCounts[website]) {
34 | websiteBrowserCounts[website] = { Chrome: 0, Firefox: 0, Safari: 0, Edge: 0 };
35 | }
36 |
37 | if (browserLabels.includes(browser)) {
38 | websiteBrowserCounts[website][browser] += 1;
39 | }
40 | });
41 |
42 | const datasets = Object.keys(websiteBrowserCounts).map(website => ({
43 | label: website,
44 | data: browserLabels.map(browser => websiteBrowserCounts[website][browser] || 0),
45 | backgroundColor: `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.2)`,
46 | borderColor: `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 1)`,
47 | borderWidth: 1,
48 | }));
49 |
50 |
51 | const radarChartData = {
52 | labels: browserLabels,
53 | datasets,
54 | };
55 |
56 | return (
57 |
58 |
59 |
Website Clicks by Browser
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default RadarChart;
72 |
--------------------------------------------------------------------------------
/client/src/components/ChartPages/Charts/ScatterChart-clicks.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react';
2 | import { Scatter } from 'react-chartjs-2';
3 | import { useAtom } from 'jotai';
4 | import { timeFrameAtom } from '../../../state/Atoms';
5 | import {
6 | Chart as ChartJS,
7 | CategoryScale,
8 | LinearScale,
9 | PointElement,
10 | Tooltip,
11 | Legend,
12 | } from 'chart.js';
13 | import styles from '../Charts.module.css';
14 | import { filterDataByTimeFrame } from "../../../services/filterDataByTimeFrame ";
15 | import { NoKeywordChart } from "../../../../types";
16 | import ChartDownload from '../ChartDownload';
17 |
18 | ChartJS.register(CategoryScale, LinearScale, PointElement, Tooltip, Legend);
19 |
20 | const ScatterChart = ({ data }: NoKeywordChart) => {
21 | const [timeFrame] = useAtom(timeFrameAtom);
22 | const chartRef = useRef(null);
23 |
24 | const filteredData = filterDataByTimeFrame(data, timeFrame);
25 |
26 | const scatterData = {
27 | datasets: [
28 | {
29 | label: 'User Clicks',
30 | data: filteredData.map((item) => ({
31 | x: item.x_coord,
32 | y: item.y_coord,
33 | })),
34 | backgroundColor: 'rgba(75,192,192,0.6)',
35 | borderColor: 'rgba(75,192,192,1)',
36 | pointRadius: 5,
37 | },
38 | ],
39 | };
40 |
41 | const options = {
42 | scales: {
43 | x: {
44 | min: 0,
45 | max: 1,
46 | title: {
47 | display: true,
48 | text: 'X Coordinate (0-1)',
49 | },
50 | },
51 | y: {
52 | min: 0,
53 | max: 1,
54 | reverse: true,
55 | title: {
56 | display: true,
57 | text: 'Y Coordinate (0-1)',
58 | },
59 | },
60 | },
61 | };
62 |
63 | return (
64 |
65 |
Scatter heatmap
66 |
67 |
68 |
69 |
70 |
71 | );
72 | };
73 |
74 | export default ScatterChart;
75 |
--------------------------------------------------------------------------------
/client/src/components/ChartPages/Charts/ScreenshotComponent.tsx:
--------------------------------------------------------------------------------
1 | // import React from 'react';
2 | import { useState, useEffect } from "react";
3 | import axios from 'axios';
4 | import {backendUrl} from '../../../state/Atoms';
5 |
6 | /*******WEBSITE SCREENSHOT REQUEST********/
7 | const ScreenshotComponent = ({pageUrl}:any) => {
8 | // store image src
9 | const [imageSrc, setImageSrc] = useState(null);
10 | const webpage = pageUrl; // <--------- webpage to screenshot!!
11 | const token = localStorage.getItem("token")!;
12 |
13 | // rerender on pageurl change
14 | useEffect(() => {
15 | // fetchImage takes in a webpage url and sets imagesrc state to be a decoded url image
16 | const fetchImage = async () => {
17 | await console.log(webpage)
18 | try {
19 | // Make a GET request to fetch the image as a binary buffer
20 | const response = await axios.get(`${backendUrl}/api/screenshot?url=${webpage}`,
21 | {
22 | headers: {
23 | Authorization: `Bearer ${token}`,
24 | },
25 | responseType: 'arraybuffer'
26 | });
27 |
28 | // Convert the ArrayBuffer to a base64-encoded string
29 | const base64String = btoa(
30 | new Uint8Array(response.data)
31 | .reduce((data, byte) => data + String.fromCharCode(byte), '')
32 | );
33 |
34 | // Construct a data URL for the image
35 | const imageSrc = `data:image/png;base64,${base64String}`;
36 |
37 | // Update the state with the image source
38 | setImageSrc(imageSrc);
39 | } catch (error) {
40 | console.error('Error fetching image:', error);
41 | }
42 | };
43 |
44 | if (pageUrl !== '') fetchImage();
45 | }, [pageUrl]);
46 |
47 | return (
48 |
49 | {imageSrc ? (
50 |
53 | ) : (
54 |
Loading image...
55 | )}
56 |
57 | );
58 | };
59 |
60 | export default ScreenshotComponent;
--------------------------------------------------------------------------------
/client/src/components/ChartPages/Charts/StackedBarGraph-clicks.tsx:
--------------------------------------------------------------------------------
1 | import { Bar } from "react-chartjs-2";
2 | import { useAtom } from "jotai";
3 | import { timeFrameAtom } from '../../../state/Atoms';
4 | import {
5 | Chart as ChartJS,
6 | BarElement,
7 | CategoryScale,
8 | LinearScale,
9 | Tooltip,
10 | Legend,
11 | } from "chart.js";
12 | import styles from '../Charts.module.css';
13 | import { filterDataByTimeFrame } from "../../../services/filterDataByTimeFrame ";
14 | import { PieChartsProps } from "../../../../types";
15 | import ChartDownload from "../ChartDownload";
16 | import { useRef } from "react";
17 | ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend);
18 |
19 | const StackedBarChart = ({ data, keyword, keywordTwo }: PieChartsProps) => {
20 | const [timeFrame] = useAtom(timeFrameAtom);
21 | const chartRef = useRef(null);
22 | const filteredData = filterDataByTimeFrame(data, timeFrame);
23 |
24 | const osBrowserCounts: { [os: string]: { [browser: string]: number } } = {};
25 |
26 | filteredData.forEach(item => {
27 | const os = item[keywordTwo];
28 | const browser = item[keyword];
29 |
30 | if (!osBrowserCounts[os]) {
31 | osBrowserCounts[os] = {};
32 | }
33 |
34 | osBrowserCounts[os][browser] = (osBrowserCounts[os][browser] || 0) + 1;
35 | });
36 |
37 | const osLabels = Object.keys(osBrowserCounts);
38 | const browserLabels = Array.from(new Set(filteredData.map(item => item[keyword])));
39 |
40 | const browserColors: { [key: string]: string } = {
41 | Chrome: 'rgba(251, 188, 5, 0.5)',
42 | Firefox: 'rgba(255, 69, 0, 0.5)',
43 | Safari: 'rgba(0, 122, 255, 0.5)',
44 | Edge: 'rgba(0, 82, 204, 0.5)',
45 | };
46 |
47 |
48 |
49 | const browserDatasets = browserLabels.map(browser => ({
50 | label: browser,
51 | data: osLabels.map(os => osBrowserCounts[os][browser] || 0),
52 | backgroundColor: browserColors[browser],
53 | borderColor: 'rgba(0, 0, 0, 0.1)',
54 | borderWidth: 1,
55 | }));
56 |
57 | const stackedBarChartData = {
58 | labels: osLabels,
59 | datasets: browserDatasets,
60 | };
61 |
62 | const options = {
63 | responsive: true,
64 | scales: {
65 | x: {
66 | stacked: true,
67 | },
68 | y: {
69 | stacked: true,
70 | },
71 | },
72 | };
73 |
74 | return (
75 |
76 |
77 |
Browser Usage by Operating System
78 |
79 |
80 |
81 |
82 |
83 |
84 | );
85 | };
86 |
87 | export default StackedBarChart;
88 |
--------------------------------------------------------------------------------
/client/src/components/ChartPages/SelectWebsiteDropdown.tsx:
--------------------------------------------------------------------------------
1 | import { useAtom } from 'jotai';
2 | import { Box, FormControl, Select, MenuItem } from "@mui/material";
3 | import styles from '../User/UserView.module.css';
4 | import { websitesAtom, activeWebsiteAtom } from '../../state/Atoms';
5 | import { useNavigate } from 'react-router-dom';
6 | import { useState } from 'react';
7 |
8 | const TimeFrameDropdown = () => {
9 | const navigate = useNavigate();
10 | const [websites] = useAtom(websitesAtom);
11 | const [, setSelectedWebsite] = useAtom(activeWebsiteAtom);
12 | const [displayedData, setDisplayedData] = useState('Overview')
13 |
14 | return (
15 |
26 |
27 | {setSelectedWebsite(e.target.value)
30 | setDisplayedData(e.target.value)
31 | }}
32 | sx={{
33 | color: "#FFFFFF",
34 | ".MuiOutlinedInput-notchedOutline": {
35 | borderColor: "transparent",
36 | },
37 | "&:hover .MuiOutlinedInput-notchedOutline": {
38 | borderColor: "686868",
39 | },
40 | "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
41 | borderColor: "#686868",
42 | },
43 | ".MuiSelect-select": {
44 | padding: "8px",
45 | },
46 | }}
47 | >
48 | {navigate("/dashboard/overview")
49 | }}>
50 | Overview
51 |
52 | {websites.map((website, index) => (
53 | {navigate(`/dashboard/${website}`)}}>{website}
54 | ))}
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default TimeFrameDropdown;
62 |
--------------------------------------------------------------------------------
/client/src/components/ChartPages/TimeFrameDropdown.tsx:
--------------------------------------------------------------------------------
1 | import { useAtom } from 'jotai';
2 | import { Box, FormControl, Select, MenuItem } from "@mui/material";
3 | import styles from '../User/UserView.module.css';
4 | import { timeFrameAtom } from '../../state/Atoms';
5 |
6 | const TimeFrameDropdown = () => {
7 | const [timeFrame, setTimeFrame] = useAtom(timeFrameAtom);
8 |
9 | return (
10 |
22 |
23 | setTimeFrame(e.target.value)}
26 | sx={{
27 | color: "#FFFFFF",
28 | ".MuiOutlinedInput-notchedOutline": {
29 | borderColor: "transparent",
30 | },
31 | "&:hover .MuiOutlinedInput-notchedOutline": {
32 | borderColor: "686868",
33 | },
34 | "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
35 | borderColor: "#686868",
36 | },
37 | ".MuiSelect-select": {
38 | padding: "8px",
39 | },
40 | }}
41 | >
42 | Last 24 Hours
43 | Last Week
44 | This Month
45 | 1 Year
46 | 5 Years
47 | All Time
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default TimeFrameDropdown;
55 |
--------------------------------------------------------------------------------
/client/src/components/ChartPages/WebsiteData.tsx:
--------------------------------------------------------------------------------
1 | import { Grid, Paper, Box } from "@mui/material";
2 | import ClickGraph from "./Charts/LineGraph-clicks";
3 | import DuelPieGraphs from "./Charts/DuelPieChart-clicks";
4 | import BarGraph from "./Charts/BarGraph-clicks";
5 | import BarGraph_referrer from "./Charts/BarGraph-referrer";
6 | import AiResponseComponent from "./Charts/aiResponse";
7 | import { websiteDataAtom, websiteReferralDataAtom } from "../../state/Atoms";
8 | import ScatterChart from "./Charts/ScatterChart-clicks";
9 | import { useAtom } from "jotai";
10 | import { mapUserData } from "../../services/extractData";
11 | import RadarChart from "./Charts/RadarGraph-clicks";
12 | import StackedBarChart from "./Charts/StackedBarGraph-clicks";
13 | import Heatmap from "./Charts/Heatmap";
14 |
15 | const WebsiteData = () => {
16 | const [websiteData] = useAtom(websiteDataAtom);
17 | const allDataResponse = mapUserData(websiteData);
18 | const [websiteReferralData] = useAtom(websiteReferralDataAtom);
19 |
20 | return (
21 |
22 |
23 |
24 |
34 |
35 |
36 |
37 |
38 |
39 |
49 |
50 |
51 |
52 |
53 |
54 |
64 |
65 |
66 |
67 |
68 |
69 |
80 |
85 |
86 |
87 |
88 |
89 |
99 |
104 |
105 |
106 |
107 |
108 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
134 |
139 |
140 |
141 |
142 |
152 |
153 |
154 |
155 |
156 |
157 |
167 |
168 |
169 |
170 |
171 |
172 |
173 | );
174 | };
175 |
176 | export default WebsiteData;
177 |
--------------------------------------------------------------------------------
/client/src/components/Documentation/Documentation.module.css:
--------------------------------------------------------------------------------
1 | .viewNoSide {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | min-height: 100vh;
7 | }
8 |
9 | .sectionWrapper {
10 | max-width: 1000px;
11 | width: 100%;
12 | padding: 20px;
13 | margin: 0 auto;
14 |
15 | }
16 |
17 | .docs {
18 | display: flex;
19 | flex-direction: column;
20 | align-items: center;
21 | }
22 |
23 | .docStep {
24 | margin-bottom: 20px;
25 | width: 100%;
26 |
27 | }
28 |
29 | .codeBlock {
30 | width: 100%;
31 | overflow-x: auto;
32 | }
33 | .codeBlockWrapper {
34 | position: relative;
35 | }
36 |
37 | .copyButton {
38 | position: flex;
39 | top: 10px;
40 | left: 880px;
41 | }
42 |
43 | .copiedText {
44 | margin-left: 8px;
45 | color: #4caf50;
46 | font-size: 12px;
47 | }
--------------------------------------------------------------------------------
/client/src/components/Footer/Footer.module.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | background: var(--blue-secondary);
3 | color: var(--white);
4 | padding: 20px;
5 | text-align: center;
6 | }
7 |
8 | .footerContent {
9 | display: flex;
10 | flex-direction: column;
11 | align-items: center;
12 | }
13 |
14 | .socialIcons {
15 | display: flex;
16 | justify-content: center;
17 | margin-bottom: 20px;
18 | }
19 |
20 | .socialIcon {
21 | display: inline-block;
22 | margin: 0 10px;
23 | vertical-align: middle;
24 | width: 34px;
25 | height: 34px;
26 | }
27 |
28 | .logo {
29 | width: 50px;
30 | height: 60px;
31 | margin-bottom: 20px;
32 | }
33 |
34 | .listContainer {
35 | display: flex;
36 | justify-content: center;
37 | width: 100%;
38 | }
39 |
40 | .list {
41 | list-style: none;
42 | padding: 0;
43 | display: flex;
44 | justify-content: center;
45 | }
46 |
47 | .list li {
48 | margin: 0 10px;
49 | }
50 |
51 | .listLink {
52 | color: #fff;
53 | text-decoration: none;
54 | }
55 |
56 | .listLink:hover {
57 | text-decoration: underline;
58 | }
--------------------------------------------------------------------------------
/client/src/components/Footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 |
3 | import styles from './Footer.module.css'; // Import the CSS module
4 |
5 | const Footer = () => {
6 | return (
7 |
8 |
9 | {/*
*/}
10 |
35 |
36 | {/*
*/}
37 |
38 |
62 |
63 |
64 |
65 |
66 |
67 |
@OSAnalytics.io
68 |
69 |
70 | );
71 |
72 | };
73 |
74 | export default Footer;
75 |
76 | {/* Dashboard icons icons created by Kharisma - Flaticon */}
77 |
78 | {/* */}
--------------------------------------------------------------------------------
/client/src/components/Landing/DashboardGif.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useRef } from "react";
2 | import {
3 | Box,
4 | Container,
5 | Grid,
6 | Typography,
7 | ThemeProvider,
8 | createTheme,
9 | } from "@mui/material";
10 | import { motion } from "framer-motion";
11 |
12 | const theme = createTheme({
13 | palette: {
14 | primary: {
15 | main: "#424242",
16 | },
17 | secondary: {
18 | main: "#757575",
19 | },
20 | background: {
21 | default: "#f5f5f5",
22 | },
23 | },
24 | });
25 |
26 | export default function DashboardPreview() {
27 | const [isVisible, setIsVisible] = useState(false);
28 | const sectionRef = useRef(null);
29 |
30 | useEffect(() => {
31 | const observer = new IntersectionObserver(
32 | (entries) => {
33 | const entry = entries[0];
34 | if (entry.isIntersecting) {
35 | setIsVisible(true);
36 | observer.disconnect();
37 | }
38 | },
39 | { threshold: 0.1 }
40 | );
41 |
42 | if (sectionRef.current) {
43 | observer.observe(sectionRef.current);
44 | }
45 |
46 | return () => {
47 | if (sectionRef.current) {
48 | observer.unobserve(sectionRef.current);
49 | }
50 | };
51 | }, []);
52 |
53 | return (
54 |
55 |
64 |
65 |
66 |
67 |
72 |
73 | Explore the Analytics Dashboard
74 |
75 |
76 | Get real-time insights and track user activities on the fly.
77 | Dive into powerful analytics and optimize your application’s
78 | performance effortlessly.
79 |
80 |
81 |
82 |
83 |
88 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | );
104 | }
105 |
--------------------------------------------------------------------------------
/client/src/components/Landing/FAQSection.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Accordion,
3 | AccordionSummary,
4 | AccordionDetails,
5 | Typography,
6 | Container,
7 | } from "@mui/material";
8 | import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
9 | import { useEffect, useState } from "react";
10 | import { motion } from "framer-motion";
11 |
12 | const faqs = [
13 | {
14 | question: "What is OS Analytics?",
15 | answer:
16 | "OS Analytics is a tool that allows developers to track and visualize user interactions on their websites in real-time using a custom click tracker.",
17 | },
18 | {
19 | question: "How do I install OS Analytics?",
20 | answer:
21 | "You can install OS Analytics by running `npm install os-analytics` and integrating the `clickTracker` hook into your React app.",
22 | },
23 | {
24 | question: "What kind of metrics can I track with OS Analytics?",
25 | answer:
26 | "You can track metrics like click events, page views, referrer events, heatmaps, and other activity such as OS and browser data.",
27 | },
28 | {
29 | question: "Is OS Analytics free to use?",
30 | answer:
31 | "Yes, OS Analytics is an open-source tool, and you can freely use it for tracking and visualizing website interactions.",
32 | },
33 | {
34 | question: "How can I configure multiple websites for tracking?",
35 | answer:
36 | "You can configure multiple websites in the OS Analytics dashboard, and you can monitor them all from one account with detailed metrics for each site.",
37 | },
38 | ];
39 |
40 | export default function FAQSection() {
41 | const [hasAnimated, setHasAnimated] = useState(false);
42 |
43 | useEffect(() => {
44 | const observer = new IntersectionObserver(
45 | (entries) => {
46 | const entry = entries[0];
47 | if (entry.isIntersecting && !hasAnimated) {
48 | setHasAnimated(true);
49 | observer.disconnect();
50 | }
51 | },
52 | { threshold: 0.1 }
53 | );
54 |
55 | const section = document.getElementById("faq-section");
56 | if (section) {
57 | observer.observe(section);
58 | }
59 |
60 | return () => {
61 | if (section) {
62 | observer.unobserve(section);
63 | }
64 | };
65 | }, [hasAnimated]);
66 |
67 | return (
68 |
69 |
78 |
83 |
84 | Frequently Asked Questions
85 |
86 | {faqs.map((faq, index) => (
87 |
88 | }
90 | aria-controls={`panel${index}-content`}
91 | id={`panel${index}-header`}
92 | >
93 | {faq.question}
94 |
95 |
96 | {faq.answer}
97 |
98 |
99 | ))}
100 |
101 |
102 |
103 | );
104 | }
105 |
--------------------------------------------------------------------------------
/client/src/components/Landing/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react";
2 | import {
3 | Box,
4 | Container,
5 | Grid,
6 | Typography,
7 | IconButton,
8 | Link,
9 | } from "@mui/material";
10 | import { GitHub, LinkedIn, Article } from "@mui/icons-material";
11 | import { motion } from "framer-motion";
12 |
13 | export default function Footer() {
14 | const [isVisible, setIsVisible] = useState(false);
15 | const footerRef = useRef(null);
16 |
17 | useEffect(() => {
18 | const observer = new IntersectionObserver(
19 | (entries) => {
20 | const entry = entries[0];
21 | if (entry.isIntersecting) {
22 | setIsVisible(true);
23 | observer.disconnect();
24 | }
25 | },
26 | { threshold: 0.1 }
27 | );
28 |
29 | if (footerRef.current) {
30 | observer.observe(footerRef.current);
31 | }
32 |
33 | return () => {
34 | if (footerRef.current) {
35 | observer.unobserve(footerRef.current);
36 | }
37 | };
38 | }, []);
39 |
40 | return (
41 |
47 |
55 |
56 |
57 |
58 |
59 | OS Analytics
60 |
61 |
62 | Open-source web analytics
63 |
64 | Simple and powerful
65 |
66 |
67 |
68 |
69 |
70 | Links
71 |
72 |
73 |
74 | Home
75 |
76 |
77 |
78 |
79 | Documentation
80 |
81 |
82 |
83 |
84 | Sign Up
85 |
86 |
87 |
88 |
89 | Sign In
90 |
91 |
92 |
93 |
94 |
95 |
96 | Connect
97 |
98 |
104 |
105 |
106 |
107 |
108 |
109 |
115 |
119 |
120 |
121 |
122 |
123 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 | © {new Date().getFullYear()} OS Analytics
139 |
140 |
141 |
142 |
143 |
144 | );
145 | }
146 |
--------------------------------------------------------------------------------
/client/src/components/Landing/Hero.module.css:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | .heroBackground {
5 | position: relative;
6 | width: 100%;
7 | height: 75vh;
8 |
9 | background-position: center;
10 | /* z-index: -1; */
11 | display: flex;
12 | justify-content: center;
13 | align-items: center;
14 | animation: bg-scale 20s infinite alternate linear;
15 | }
16 |
17 | /* .heroBackground::after {
18 | position: absolute;
19 | height: 100%;
20 | width: 100%;
21 | content: '';
22 | background-image: linear-gradient(
23 | -45deg,
24 | rgba(37, 180, 44, 0),
25 | rgba(37, 180, 44, 0),
26 | rgba(168, 32, 125, 0.1),
27 | rgba(168, 32, 125, 0.15),
28 | rgba(200, 34, 34, 0.15),
29 | rgba(200, 34, 34, 0.1),
30 | rgba(168, 32, 125, 0.1),
31 | rgba(168, 32, 125, 0.1),
32 | rgba(37, 180, 44, 0),
33 | rgba(37, 180, 44, 0),
34 | rgba(37, 180, 44, 0)
35 | );
36 | background-size: 1000%;
37 | animation: bg-animation 10s infinite;
38 | } */
39 |
40 | /* rgba(200, 34, 34, 0.3),
41 | rgba(255, 247, 0, 0.3),
42 | rgba(168, 32, 125, 0.3),
43 | rgba(34, 129, 129, 0.3), */
44 |
45 | .hero {
46 | width: 100%;
47 | height: 100%;
48 | max-width: 1400px;
49 | text-align: left;
50 | display: flex;
51 | align-items: center;
52 | justify-content: center;
53 | gap: 1.6rem;
54 | padding: 8rem 1rem;
55 | background-size: cover;
56 | background-position: top left;
57 | z-index: 5;
58 | position: absolute;
59 | color: white;
60 |
61 |
62 | }
63 |
64 | .heroLeft {
65 | display: flex;
66 | flex-direction: column;
67 | justify-content: center;
68 | gap:2rem;
69 | width: 50%;
70 | }
71 |
72 | .heroRight {
73 | display: flex;
74 | align-items: center;
75 | width: 50%;
76 | }
77 |
78 | .heroDemo {
79 | height: 50vh;
80 | }
81 |
82 | .hero h4 {
83 | max-width: 70ch;
84 | color: var(--white);
85 | }
86 | .heroButtons {
87 | display: flex;
88 | gap: 0.8rem;
89 | align-items: center;
90 | justify-content: flex-start;
91 | }
92 |
93 | /* .fancy {
94 | color: transparent;
95 | background-image: var(--gradient);
96 | background-size: 1200%;
97 | animation: bg-animation 5s infinite alternate;
98 | background-clip: text;
99 | } */
100 |
101 |
102 | @keyframes bg-animation {
103 | 0% {
104 | background-position: left;
105 | }
106 | 100% {
107 | background-position: right;
108 | }
109 | }
110 |
111 | @keyframes bg-scale {
112 | 0% {
113 | background-size: 100%;
114 | }
115 | 100% {
116 | background-size: 120%;
117 | }
118 | }
119 |
120 |
121 | @media only screen and (max-width: 900px) {
122 | .hero {
123 | flex-direction: column;
124 | align-items: center;
125 | padding: 2em 3rem;
126 | }
127 | .heroLeft {
128 | width: 100%;
129 | }
130 | .heroRight {
131 | width: 100%;
132 | }
133 |
134 |
135 | }
--------------------------------------------------------------------------------
/client/src/components/Landing/Hero.tsx:
--------------------------------------------------------------------------------
1 | import styles from '../Landing/Hero.module.css';
2 | import demoImg from '../../assets/dashboard-google-analytics-sexy.png';
3 | import { useEffect, useRef } from 'react';
4 | import NET from 'vanta/dist/vanta.net.min';
5 | import * as THREE from 'three';
6 | import { Link } from 'react-router-dom';
7 | import { motion } from 'framer-motion';
8 |
9 | export default function Hero() {
10 | const vantaRef = useRef(null);
11 |
12 | useEffect(() => {
13 | const vantaEffect = NET({
14 | el: vantaRef.current,
15 | mouseControls: false,
16 | touchControls: false,
17 | gyroControls: false,
18 | minHeight: 200.0,
19 | minWidth: 200.0,
20 | scale: 1.0,
21 | scaleMobile: 1.0,
22 | color: 0x3fafff,
23 | backgroundColor: 0x1c1a4a,
24 | THREE: THREE,
25 | });
26 |
27 | return () => {
28 | if (vantaEffect) vantaEffect.destroy();
29 | };
30 | }, []);
31 |
32 | return (
33 |
34 |
35 |
41 | Open Source Website Analytic Toolkit For Developers
42 |
43 | OS Analytics is an open source, developer friendly tool for tracking
44 | the traffic and user interactions of any deployed application. Our goal is to give developers the tools they need to monitor user interactions without relying on third party products.
45 |
46 |
52 |
53 |
54 | Create account
55 |
56 |
57 |
58 |
59 |
60 | Documentation
61 |
62 |
63 |
64 |
65 |
66 |
72 |
73 |
74 |
75 |
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/client/src/components/Landing/LandingView.tsx:
--------------------------------------------------------------------------------
1 | import Navbar from "../Navbar/Navbar";
2 | import Hero from "./Hero";
3 | import NavMobile from "../Navbar/NavMobile";
4 | import WhyOSA from "./WhyOSA";
5 | import TeamSection from "./githubProfiles";
6 | import FAQSection from "./FAQSection";
7 | import DashboardPreview from "./DashboardGif";
8 | import GettingStarted from "./GettingStarted";
9 | import Footer from "./Footer";
10 |
11 | export default function Landing() {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/components/Landing/WhyOSA.module.css:
--------------------------------------------------------------------------------
1 | .whyOSA {
2 | width: 100%;
3 | background: #f4f4f4;
4 | display: flex;
5 | justify-content: center;
6 | flex-direction: column;
7 | align-items: center;
8 | padding: 2rem 3rem;
9 | gap: 1rem;
10 | }
11 | /* .whyOSA h1, h2, h3, h4, p {
12 | color: var(--black);
13 | } */
14 |
15 | .whyGrid {
16 | width: 100%;
17 | max-width: 1400px;
18 | display: grid;
19 | grid-template-rows: 1fr 1fr;
20 | grid-template-columns: 1fr 1fr;
21 | gap: 1rem;
22 |
23 | @media only screen and (max-width: 850px) {
24 | grid-template-rows: 1fr;
25 | grid-template-columns: 1fr;
26 | }
27 | }
28 |
29 | .whyItem {
30 | display: flex;
31 | gap: 1rem;
32 | justify-content: space-between;
33 | align-items: center;
34 | }
35 |
36 | .iconBox {
37 | padding: 2rem;
38 | }
39 | /* .whyContentBox {
40 | flex-grow: true;
41 | } */
42 |
43 | .whyIcon {
44 | height: 4rem;
45 |
46 | @media only screen and (max-width: 850px) {
47 | height: 2.4rem;
48 | }
49 | }
--------------------------------------------------------------------------------
/client/src/components/Landing/WhyOSA.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react';
2 | import styles from './WhyOSA.module.css';
3 | import icon from '../../assets/icons/graphs.png';
4 | import globe from '../../assets/icons/blackGlobe.png';
5 | import aiImage from '../../assets/icons/aiImage.png';
6 | import clickImage from '../../assets/icons/clickImage.png';
7 | import { motion } from 'framer-motion';
8 |
9 | export default function WhyOSA() {
10 | const [isVisible, setIsVisible] = useState(false);
11 | const sectionRef = useRef(null);
12 |
13 |
14 | useEffect(() => {
15 | const observer = new IntersectionObserver(
16 | (entries) => {
17 | const entry = entries[0];
18 | if (entry.isIntersecting) {
19 | setIsVisible(true);
20 | observer.disconnect();
21 | }
22 | },
23 | { threshold: 0.2 }
24 | );
25 |
26 | if (sectionRef.current) {
27 | observer.observe(sectionRef.current);
28 | }
29 |
30 | return () => {
31 | if (sectionRef.current) {
32 | observer.unobserve(sectionRef.current);
33 | }
34 | };
35 | }, []);
36 |
37 | return (
38 |
39 |
45 | Why OS Analytics?
46 |
47 |
48 | {[{
49 | img: clickImage,
50 | title: 'Custom Click Tracking',
51 | description: 'Easily integrate our clickTracker to monitor user clicks and interactions.'
52 | }, {
53 | img: icon,
54 | title: 'Real-Time Analytics',
55 | description: 'Capture and visualize user activity data in real-time using our robust dashboard.'
56 | }, {
57 | img: globe,
58 | title: 'Multiple Website Support',
59 | description: 'Track and manage multiple websites effortlessly through the dashboard.'
60 | }, {
61 | img: aiImage,
62 | title: 'AI-Powered Reports',
63 | description: 'Leverage AWS Bedrock for generating insightful and comprehensive activity reports.'
64 | }].map((item, index) => (
65 |
72 |
73 |
74 |
75 |
76 |
77 | {item.title}
78 |
79 |
80 | {item.description}
81 |
82 |
83 |
84 | ))}
85 |
86 |
87 | );
88 | }
89 |
--------------------------------------------------------------------------------
/client/src/components/Landing/githubProfiles.tsx:
--------------------------------------------------------------------------------
1 | import { Grid, Card, CardContent, Typography, Avatar, Button } from '@mui/material';
2 | import GitHubIcon from '@mui/icons-material/GitHub';
3 | import LinkedInIcon from '@mui/icons-material/LinkedIn';
4 | import { useEffect, useState } from 'react';
5 | import { motion } from 'framer-motion';
6 | import ericImage from '../../assets/team/eric.png';
7 | import peterImage from '../../assets/team/peter.png';
8 | import davidImage from '../../assets/team/david.png';
9 | import sawImage from '../../assets/team/saw.png';
10 |
11 | const teamMembers = [
12 | {
13 | name: 'Eric DiMarzio',
14 | github: 'https://github.com/EricDiMarzio',
15 | linkedin: 'https://www.linkedin.com/in/ericdimarzio/',
16 | image: ericImage,
17 | },
18 | {
19 | name: 'Peter Larcheveque',
20 | github: 'https://github.com/plarchev',
21 | linkedin: 'https://linkedin.com/in/peter-larcheveque/',
22 | image: peterImage,
23 | },
24 | {
25 | name: 'David Naymon',
26 | github: 'https://github.com/DavidN22',
27 | linkedin: 'https://www.linkedin.com/in/david-naymon-76520018a/',
28 | image: davidImage,
29 | },
30 | {
31 | name: 'Saw Yan Naing',
32 | github: 'https://github.com/willsyn7',
33 | linkedin: 'https://www.linkedin.com/in/saw-naing/',
34 | image: sawImage,
35 | },
36 | ];
37 |
38 | export default function TeamSection() {
39 | const [isVisible, setIsVisible] = useState(false);
40 |
41 | useEffect(() => {
42 | const observer = new IntersectionObserver(
43 | (entries) => {
44 | const entry = entries[0];
45 | if (entry.isIntersecting) {
46 | setIsVisible(true);
47 | observer.disconnect();
48 | }
49 | },
50 | { threshold: 0.1 }
51 | );
52 |
53 | const section = document.getElementById('team-section');
54 | if (section) {
55 | observer.observe(section);
56 | }
57 |
58 | return () => {
59 | if (section) {
60 | observer.unobserve(section);
61 | }
62 | };
63 | }, []);
64 |
65 | return (
66 |
123 | );
124 | }
--------------------------------------------------------------------------------
/client/src/components/Loading/Loading.module.css:
--------------------------------------------------------------------------------
1 | /* body{
2 | background-color: rgba(255, 255, 255, 0.87);
3 | margin: 0px;
4 | padding: 0px;
5 | } */
6 |
7 | /* .cup{
8 | height: 140px;
9 | width : 180px;
10 | border:6px solid rgba(255, 255, 255, 0.87);
11 | position:absolute;
12 | top: 40%;
13 | left: 40%;
14 | border-radius: 0px 0px 70px 70px;
15 | /* background:url('../../assets/coffee.png'); ; */
16 | /* background : url;
17 |
18 | box-shadow: 0px 0px 0px 6px rgba(255, 255, 255, 0.87);
19 | background-repeat: repeat-x;
20 | background-position: 0px 140px;
21 | animation: fill 2.5s infinite; */
22 | /* }
23 | .liquid-ctr{
24 | top: -10px;
25 | width : 250px;
26 | height : 400px;
27 | overflow: hidden;
28 | back */
29 | /* } */
30 | .cup{
31 | height: 250px;
32 | width : 180px;
33 | border:6px solid rgba(255, 255, 255, 0.87);
34 | position:absolute;
35 | border-top: 5px solid;
36 | top: 50%;
37 | left: 50%;
38 | border-radius: 15px;
39 | border-top-left-radius: 5px;
40 | border-top-right-radius: 5px;
41 | text-transform: uppercase;
42 | position: relative;
43 | overflow:hidden;
44 | z-index : 1;
45 | }
46 | .cup:before{
47 | content: "";
48 | position: absolute;
49 | width : 400px;
50 | height : 400px;
51 | background : #43a047;
52 | left : 50%;
53 | transform: translateX(-50%);
54 | border-radius: 40%;
55 | animation: fill 7s ease-in-out infinite;
56 | z-index: -1;
57 |
58 | }
59 |
60 |
61 |
62 | @keyframes fill{
63 | from{
64 | top: 245px;
65 | transform: translateX(-50%) rotate(0deg);
66 | }
67 | to{
68 | top: -50px;
69 | transform: translateX(-50%) rotate(360deg);
70 | }
71 | }
72 | .handle {
73 | height: 70px;
74 | width: 40px;
75 | background-color: transparent;
76 | border: 6px solid rgba(255, 255, 255, 0.87);
77 | position: absolute;
78 | left: 226px;
79 | top: 42%;
80 | border-radius: 0px 25px 80px 0px;
81 | z-index: 2;
82 | }
--------------------------------------------------------------------------------
/client/src/components/Loading/Loading.tsx:
--------------------------------------------------------------------------------
1 | import styles from './Loading.module.css';
2 |
3 | export default function Loading(){
4 | return (
5 |
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/components/Login/Login.module.css:
--------------------------------------------------------------------------------
1 | .loginPage {
2 | display: flex;
3 | height: calc(100vh - 4rem);
4 | overflow: hidden;
5 | }
6 |
7 | .login {
8 |
9 | display: flex;
10 | flex-direction: column;
11 | max-width: 100vw;
12 | width: 60rem;
13 | justify-content: center;
14 | align-items: center;
15 | padding: 4.8rem;
16 | gap: 1.2rem;
17 | background: #ffffff;
18 | }
19 |
20 | .loginBackground {
21 | height: calc(100vw-4rem);
22 | width: 100%;
23 | display: flex;
24 | flex-direction: column;
25 | align-items: center;
26 | place-content: center;
27 |
28 | }
29 | .typewriter h1 {
30 | overflow: hidden;
31 | border-right: .15em solid rgb(243, 243, 242);
32 | color: #fff;
33 | white-space: nowrap;
34 | margin: 0 auto;
35 | letter-spacing: .10em;
36 | animation:
37 | typing 3.5s steps(40, end),
38 | blink-caret .75s step-end infinite;
39 | }
40 |
41 | @keyframes typing {
42 | from { width: 0 }
43 | to { width: 100% }
44 | }
45 |
46 | @keyframes blink-caret {
47 | from, to { border-color: transparent }
48 | 50% { border-color: rgb(245, 244, 242); }
49 | }
50 |
51 | .imageWrapper {
52 | margin-top: 20px;
53 | display: flex;
54 | justify-content: center;
55 | }
56 |
57 | .demoImg {
58 | max-width: 90%;
59 | border-radius: 10px;
60 | }
61 | .login h2 {
62 | font-weight: 300;
63 | }
64 | .oathButtons {
65 | display: flex;
66 | flex-direction: column;
67 | width: 100%;
68 | gap: 1.6rem;
69 | }
70 |
71 | .loginBtn {
72 | width: 100%;
73 | }
74 |
75 | .google {
76 | background: rgb(243, 243, 243);
77 | color: var(--black);
78 | }
79 | .google:hover,.github:hover {
80 | border: 2px solid var(--blue-primary);
81 | }
82 | .github {
83 | background: rgb(194, 194, 194);
84 | color: var(--black);
85 | }
86 |
87 | .loginCredentials {
88 | display: flex;
89 | flex-direction: column;
90 | gap: 1.6rem;
91 | width: 100%;
92 | }
93 |
94 | .createAccountQuery {
95 | display: flex;
96 | flex-direction: column;
97 | gap:0;
98 | }
99 | .createAccountQuery p {
100 | line-height: .6rem;
101 | text-align: center;
102 | font-weight: 300;
103 | color: var(—black);
104 | }
105 |
106 | .createAccountQuery a {
107 | font-weight: 300;
108 | }
109 |
110 | .createAccountQuery a:hover {
111 | color: var(—black);
112 | }
113 |
114 |
115 | @media only screen and (max-width: 800px) {
116 | .login {
117 | width: 100vw;
118 | }
119 | .loginBackground {
120 | display: none;
121 | }
122 | }
123 |
124 |
--------------------------------------------------------------------------------
/client/src/components/Login/Signup.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import styles from "./Login.module.css";
3 | import axios from "axios";
4 | import { useAtom } from "jotai";
5 | import { activeUserAtom, backendUrl } from "../../state/Atoms";
6 | import Navbar from "../Navbar/Navbar";
7 | import NavMobile from "../Navbar/NavMobile";
8 | import BarAnimation from "../Animations/BarAnimation";
9 | import * as THREE from 'three';
10 | import GLOBE from 'vanta/dist/vanta.globe.min';
11 | import { Link, useNavigate } from "react-router-dom";
12 | import Alert from '@mui/material/Alert';
13 | import { Button } from "@mui/material";
14 | import GoogleIcon from "@mui/icons-material/Google";
15 | import { motion } from "framer-motion";
16 |
17 | export default function Signup() {
18 | const [, setActiveUser] = useAtom(activeUserAtom);
19 | const [errorMessage, setErrorMessage] = useState('')
20 | const [formData, setFormData] = useState({
21 | email: "",
22 | password: "",
23 | confirmPassword: "",
24 | });
25 | const navigate = useNavigate();
26 | const vantaRef = useRef(null);
27 |
28 | useEffect(() => {
29 | const vantaEffect = GLOBE({
30 | el: vantaRef.current,
31 | mouseControls: false,
32 | touchControls: false,
33 | gyroControls: false,
34 | minHeight: 100.00,
35 | minWidth: 100.00,
36 | scale: 1.00,
37 | scaleMobile: 1.00,
38 | color: 0x3fafff,
39 | backgroundColor: 0x1c1a4a,
40 | THREE: THREE,
41 | });
42 |
43 | return () => {
44 | if (vantaEffect) vantaEffect.destroy();
45 | };
46 | }, []);
47 |
48 | function handleChange(e: React.ChangeEvent) {
49 | setFormData({
50 | ...formData,
51 | [e.target.name]: e.target.value,
52 | });
53 | }
54 |
55 | async function handleSubmit(e: React.FormEvent) {
56 | e.preventDefault();
57 | if (formData.password !== formData.confirmPassword) {
58 | alert("Passwords do not match");
59 | return;
60 | }
61 |
62 | try {
63 | const response = await axios.post(`${backendUrl}/api/auth/signup`, formData);
64 | setActiveUser(response.data.email);
65 | localStorage.setItem("token", response.data.token);
66 | navigate('/dashboard');
67 | } catch (err: any) {
68 | setErrorMessage(err.response.data)
69 | }
70 | }
71 |
72 | return (
73 |
74 |
75 |
76 |
82 |
88 | Create Account
89 | {errorMessage && (
90 |
91 | {errorMessage}
92 |
93 | )}
94 |
95 |
144 |
145 |
151 | (window.location.href = "/api/google")}
153 | variant="contained"
154 | color="primary"
155 | startIcon={ }
156 | fullWidth
157 | >
158 | Continue with Google
159 |
160 |
161 |
162 |
163 |
164 |
Already have an account?
165 |
166 | Sign in here
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 | );
175 | }
176 |
--------------------------------------------------------------------------------
/client/src/components/Navbar/NavMobile.module.css:
--------------------------------------------------------------------------------
1 | .navMobile {
2 | display: none;
3 | height: calc(100vh - 4rem);
4 | width: 100vw;
5 | z-index: 1300;
6 | background: var(--black);
7 | background: linear-gradient(
8 | to left, var(--blue-primary),
9 | var(--blue-secondary)
10 | );
11 | position: absolute;
12 | /* padding-top: 3.2rem; */
13 | position: fixed;
14 | left: 0;
15 | top: 4rem;
16 | transition: all 0.25s ease;
17 | color: white;
18 | }
19 |
20 | .mobileLinks a:hover {
21 | color: var(--primary-blue);
22 | }
23 |
24 | .offScreenLeft {
25 | transform: translateX(-100%);
26 | }
27 |
28 | .mobileLinks {
29 | width: 100%;
30 | list-style: none;
31 | height: 100%;
32 | }
33 |
34 | .mobileLink {
35 | border-bottom: 1px inset var(--gray-border);
36 | padding-left: 3.2rem;
37 | display: flex;
38 | align-items: center;
39 | height: 4.8rem;
40 | cursor: pointer;
41 | }
42 | .mobileLink:hover {
43 | background: var(--gray-border);
44 | color: var(--primary-blue);
45 | }
46 |
47 | .dashboardLink {
48 | }
49 |
50 | @media only screen and (max-width: 850px) {
51 | .navMobile {
52 | display: block;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/client/src/components/Navbar/NavMobile.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import styles from './NavMobile.module.css';
3 | import { useAtom } from 'jotai';
4 | import { activeNavAtom, activeUserAtom } from '../../state/Atoms';
5 | import { handleLogout } from '../../services/authConfig';
6 |
7 | export default function NavMobile() {
8 | const [activeNav, setActiveNav] = useAtom(activeNavAtom);
9 | const [activeUser, setActiveUser] = useAtom(activeUserAtom);
10 |
11 | const onLogoutClick = async () => {
12 | const success = await handleLogout();
13 | if (success!) {
14 | setActiveUser('');
15 | }
16 | };
17 |
18 | return (
19 |
26 |
27 | {activeUser ? (
28 | <>
29 | {
32 | setActiveNav(false);
33 | }}
34 | >
35 | {' '}
36 |
37 | Home
38 |
39 |
40 | {
43 | setActiveNav(false);
44 | }}
45 | >
46 |
47 | Dashboard
48 |
49 |
50 |
51 | {
54 | setActiveNav(false);
55 | }}
56 | >
57 | {' '}
58 |
59 | Documentation
60 |
61 |
62 | {
65 | setActiveNav(false);
66 | }}
67 | >
68 |
69 | Settings
70 |
71 |
72 |
73 | {
76 | setActiveNav(false);
77 | }}
78 | >
79 |
80 | Playground
81 |
82 |
83 |
84 | {
87 | setActiveNav(false);
88 | onLogoutClick();
89 | }}
90 | >
91 | {' '}
92 |
93 | Sign out
94 |
95 |
96 | >
97 | ) : (
98 | <>
99 | {
102 | setActiveNav(false);
103 | }}
104 | >
105 | {' '}
106 |
107 | Home
108 |
109 |
110 |
111 | {
114 | setActiveNav(false);
115 | }}
116 | >
117 | {' '}
118 |
119 | Documentation
120 |
121 |
122 |
123 |
124 |
125 | {
128 | setActiveNav(false);
129 | }}
130 | >
131 | {' '}
132 |
133 | Login
134 |
135 |
136 | {
139 | setActiveNav(false);
140 | }}
141 | >
142 | {' '}
143 |
144 | Create account
145 |
146 |
147 | >
148 | )}
149 |
150 |
151 | );
152 | }
153 |
--------------------------------------------------------------------------------
/client/src/components/Navbar/Navbar.module.css:
--------------------------------------------------------------------------------
1 | .navbar {
2 | border-bottom: 1px solid var(--gray-border);
3 | position: fixed;
4 | width: 100vw;
5 | height: 4rem;
6 | background: var(--blue-secondary);
7 | top: 0;
8 | left: 0;
9 | display: grid;
10 | place-content: center;
11 | box-shadow: 0 1px 4px 3px rgb(30, 29, 37);
12 | z-index: 10;
13 | }
14 | .navbar h3{
15 | color: var(--white);
16 | }
17 |
18 | .navContainer {
19 | display: flex;
20 | justify-content: space-between;
21 | width: 100vw;
22 | max-width: 1400px;
23 | padding-right: 1.2rem;
24 | }
25 |
26 | .navLeft {
27 | display: flex;
28 | align-items: center;
29 | gap: 2rem;
30 | }
31 |
32 | .navLeftDash {
33 | display: flex;
34 | align-items: center;
35 | gap: 2rem;
36 | margin-left: 5rem;
37 | }
38 |
39 | .navLogo {
40 | height: 2.4rem;
41 | }
42 | .logoBox {
43 | display: flex;
44 | align-items: center;
45 | gap: 0.5rem;
46 |
47 | }
48 |
49 | .navLinks {
50 | display: flex;
51 | gap: 1rem;
52 | }
53 | .navRight {
54 | display: flex;
55 | align-items: center;
56 | gap: 0.8rem;
57 | }
58 | .navButton {
59 | height: 2.4rem;
60 | text-wrap: nowrap;
61 | font-size: 1rem;
62 | }
63 | .gitBox {
64 | display: flex;
65 | align-items: center;
66 | gap: .4rem;
67 | height: 100%;
68 | }
69 |
70 | .logOutIcon {
71 | height: 1.6rem;
72 | padding-top: 3px;
73 | }
74 |
75 | .hamburgerBox {
76 | display: none;
77 | height: 100%;
78 | display: none;
79 | cursor: pointer;
80 | }
81 |
82 | .hamburger {
83 | width: 40px;
84 | height: 4px;
85 | background: var(--blue-primary);
86 | border-radius: 100px;
87 | position: relative;
88 | }
89 |
90 | .hamburger::after,
91 | .hamburger::before {
92 | width: 100%;
93 | height: 100%;
94 | border-radius: 100px;
95 | background: var(--blue-primary);
96 | content: '';
97 | position: absolute;
98 | top: 0;
99 | left: 0;
100 | transition: all 0.25s ease;
101 | }
102 |
103 | .hamburger::before {
104 | transform: translateY(-0.55rem);
105 | }
106 |
107 | .hamburger::after {
108 | transform: translateY(0.55rem);
109 | }
110 |
111 | .activeNav .hamburger::after {
112 | transform: translateY(.7rem);
113 | /* background: red; */
114 | }
115 |
116 | .activeNav .hamburger::before {
117 | transform: translateY(-.7rem);
118 | /* background: red; */
119 | }
120 |
121 | .timeframe {
122 |
123 | width: 10.5rem;
124 | text-align: left;
125 | /* border: 1px solid var(--blue-secondary); */
126 | }
127 |
128 |
129 | @media only screen and (max-width: 850px) {
130 |
131 | .navLeftDash {
132 | margin-left: 1rem;
133 | }
134 | .navLinks {
135 | display: none;
136 | }
137 | .navRight {
138 | display: none;
139 | }
140 | .hamburgerBox {
141 | display: grid;
142 | place-content: center;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/client/src/components/Navbar/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import styles from './Navbar.module.css';
2 | import logo from '../../assets/icons/pie-chart.png';
3 | import { Link, NavLink } from 'react-router-dom';
4 | import { useAtom } from 'jotai';
5 | import { activeUserAtom, activeNavAtom } from '../../state/Atoms';
6 | import { handleLogout } from '../../services/authConfig';
7 | export default function Navbar() {
8 | const [activeUser, setActiveUser] = useAtom(activeUserAtom);
9 | const [activeNav, setActiveNav] = useAtom(activeNavAtom);
10 |
11 | const onLogoutClick = async () => {
12 | const success = await handleLogout();
13 | if (success!) {
14 | setActiveUser('');
15 | }
16 | };
17 | return (
18 |
19 |
20 |
21 |
{
24 | setActiveNav(false);
25 | }}
26 | >
27 |
28 |
29 |
OS Analytics
30 |
31 |
32 | {activeUser ? (
33 |
34 |
35 | Documentation
36 |
37 |
38 | Settings
39 |
40 |
41 | Playground
42 |
43 |
44 | ) : (
45 |
46 |
47 | Documentation
48 |
49 |
50 | Getting Started
51 |
52 |
53 | )}
54 |
55 |
56 | {activeUser ? (
57 | <>
58 |
59 |
60 |
61 |
62 | GitHub
63 |
64 |
65 |
66 | Dashboard
67 |
68 |
69 |
{
72 | onLogoutClick();
73 | }}
74 | >
75 |
76 | Log out
77 |
78 |
79 | >
80 | ) : (
81 | <>
82 |
83 |
84 |
85 |
86 | GitHub
87 |
88 |
89 | Create account
90 |
91 |
92 |
93 |
94 | Log in
95 |
96 |
97 |
98 | >
99 | )}
100 |
101 |
{
108 | setActiveNav(activeNav === false ? true : false);
109 | }}
110 | >
111 |
112 |
113 |
114 |
115 | );
116 | }
117 |
--------------------------------------------------------------------------------
/client/src/components/Navbar/NavbarDashboard.tsx:
--------------------------------------------------------------------------------
1 | import styles from './Navbar.module.css';
2 | import { Link } from 'react-router-dom';
3 | import { useAtom } from 'jotai';
4 | import { activeUserAtom, activeNavAtom } from '../../state/Atoms';
5 | import { handleLogout } from '../../services/authConfig';
6 | import TimeFrameDropdown from '../ChartPages/TimeFrameDropdown';
7 | import SelectWebsiteDropdown from '../ChartPages/SelectWebsiteDropdown';
8 | export default function NavbarDashboard() {
9 | const [activeUser, setActiveUser] = useAtom(activeUserAtom);
10 | const [activeNav, setActiveNav] = useAtom(activeNavAtom);
11 |
12 | const onLogoutClick = async () => {
13 | const success = await handleLogout();
14 | if (success!) {
15 | setActiveUser('');
16 | }
17 | };
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 | {activeUser ? (
26 |
27 |
28 | Dashboard
29 |
30 |
31 | Documentation
32 |
33 |
34 | Settings
35 |
36 |
37 | Playground
38 |
39 |
40 | ) : (
41 |
42 |
43 | Documentation
44 |
45 |
46 | Getting Started
47 |
48 |
49 | )}
50 |
51 |
52 | {activeUser ? (
53 | <>
54 |
55 |
56 |
57 |
58 | GitHub
59 |
{activeUser.split('@')[0].toUpperCase()}
60 |
{
63 | onLogoutClick();
64 | }}
65 | >
66 |
69 | Log out
70 |
71 |
72 | >
73 | ) : (
74 | <>
75 |
76 |
79 | Sign in
80 |
81 |
82 |
83 |
86 | Create account
87 |
88 |
89 | >
90 | )}
91 |
92 |
{
99 | setActiveNav(activeNav === false ? true : false);
100 | }}
101 | >
102 |
103 |
104 |
105 |
106 | );
107 | }
108 |
--------------------------------------------------------------------------------
/client/src/components/PageNotFound/PageNotFound.module.css:
--------------------------------------------------------------------------------
1 | .container{
2 | background-color:#8b949e;
3 | position: fixed;
4 | display: flex;
5 | justify-content: center;
6 | align-items:center;
7 | height:100vh;
8 | text-align : center;
9 | }
10 | .heading{
11 | color : rgba(255, 255, 255, 0.87)
12 | }
13 | p{
14 | font-size : 1.5em;
15 | margin : 0 ;
16 | margin-top: 20px;
17 | }
18 | .link{
19 | display: inline-block;
20 | padding: 10px 20px;
21 | background-color: #aff5b4;
22 | color: rgba(255, 255, 255, 0.87);
23 | text-decoration: none;
24 | margin-top: 20px;
25 | font-size: 1.2em;
26 | border-radius: 50px;
27 | animation: bounce 1s ease-in-out infinite;
28 | }
29 | @keyframes bounce{
30 | 0%, 20%, 50%, 80%, 100% {
31 | transform: translateY(0);
32 | }
33 | 40%{
34 | transform: translateY*(-10px);
35 | }
36 | 60%{
37 | transform: translateY*(-5px);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/client/src/components/PageNotFound/PageNotFound.tsx:
--------------------------------------------------------------------------------
1 | import styles from './PageNotFound.module.css';
2 | import { Link } from 'react-router-dom';
3 |
4 | export default function PageNotFound() {
5 | return (
6 |
7 |
404 - Page not found
8 |
Would you like to go back to the home page?
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/components/Playground/FrequencyReactFlow/FlowPlayground.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import {
3 | ReactFlow,
4 | useNodesState,
5 | useEdgesState,
6 | Background,
7 | Node,
8 | Edge,
9 | } from "@xyflow/react";
10 | import "@xyflow/react/dist/style.css";
11 | import { useAtom } from "jotai";
12 | import { userDataAtom } from "../../../state/Atoms";
13 | import { FrequencyProps, AggregatedData, QueryData } from "../../../../types";
14 |
15 | const FlowDiagram = ({ selectedWebsite, selectedPage }: FrequencyProps) => {
16 | const [userData] = useAtom(userDataAtom);
17 |
18 | const [nodes, setNodes, onNodesChange] = useNodesState([]);
19 | const [edges, setEdges, onEdgesChange] = useEdgesState([]);
20 |
21 | const pageWidth = 1980;
22 | const pageHeight = 1080;
23 |
24 | useEffect(() => {
25 | if (!selectedWebsite || !selectedPage) {
26 | setNodes([]);
27 | setEdges([]);
28 | return;
29 | }
30 |
31 | const filteredData = userData.filter(
32 | (interaction: QueryData) =>
33 | interaction.dataset_id !== null &&
34 | interaction.website_name === selectedWebsite &&
35 | interaction.page_url === selectedPage
36 | );
37 | const aggregatedData: AggregatedData = {};
38 |
39 | filteredData.forEach((interaction: QueryData) => {
40 | const key = interaction.dataset_id;
41 |
42 | if (!aggregatedData[key]) {
43 | aggregatedData[key] = {
44 | dataset_id: interaction.dataset_id,
45 | x_coord: interaction.x_coord,
46 | y_coord: interaction.y_coord,
47 | count: 0,
48 | };
49 | }
50 | aggregatedData[key].count += 1;
51 | });
52 |
53 | const pageNode: Node = {
54 | id: "page",
55 | data: {
56 | label: selectedPage,
57 | },
58 | position: { x: 0, y: 0 },
59 | style: {
60 | width: `${pageWidth}px`,
61 | height: `${pageHeight}px`,
62 | backgroundColor: "#ffffff",
63 | border: "2px solid #000",
64 | borderRadius: "10px",
65 | display: "flex",
66 | justifyContent: "center",
67 | alignItems: "center",
68 | fontSize: "24px",
69 | fontWeight: "bold",
70 | color: "#333",
71 | zIndex: -1,
72 | },
73 | };
74 |
75 | const initialNodes: Node[] = Object.keys(aggregatedData).map(
76 | (dataset_id) => {
77 | const { x_coord, y_coord, count } = aggregatedData[dataset_id];
78 | console.log(x_coord)
79 | const pixelX = x_coord * pageWidth;
80 | const pixelY = y_coord * pageHeight;
81 |
82 | return {
83 | id: dataset_id,
84 | data: {
85 | label: `Button (${count} presses)`,
86 | },
87 | position: {
88 | x: pixelX,
89 | y: pixelY,
90 | },
91 | style: {
92 | backgroundColor: `rgba(255, 0, 0, ${Math.min(count / 10, 1)})`,
93 | borderRadius: "8px",
94 | padding: "10px 20px",
95 | color: "#fff",
96 | border: "2px solid #ccc",
97 | boxShadow: "0 4px 10px rgba(0, 0, 0, 0.15)",
98 | },
99 | };
100 | }
101 | );
102 |
103 | setNodes([pageNode, ...initialNodes]);
104 | setEdges([]);
105 | }, [userData, selectedWebsite, selectedPage, setNodes, setEdges]);
106 |
107 | return (
108 |
117 |
125 |
126 |
127 |
128 | );
129 | };
130 |
131 | export default FlowDiagram;
132 |
--------------------------------------------------------------------------------
/client/src/components/Playground/OverallReactFlow/Layout.tsx:
--------------------------------------------------------------------------------
1 | import dagre from 'dagre';
2 | import { Node, Edge } from "@xyflow/react";
3 |
4 | const nodeWidth = 150;
5 | const nodeHeight = 50;
6 |
7 | const getLayoutedElements = (nodes: Node[], edges: Edge[], direction = 'TB') => {
8 | const dagreGraph = new dagre.graphlib.Graph();
9 | dagreGraph.setDefaultEdgeLabel(() => ({}));
10 | dagreGraph.setGraph({ rankdir: direction });
11 |
12 | nodes.forEach((node) => {
13 | dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
14 | });
15 |
16 | edges.forEach((edge) => {
17 | dagreGraph.setEdge(edge.source, edge.target);
18 | });
19 |
20 | dagre.layout(dagreGraph);
21 |
22 | nodes.forEach((node) => {
23 | const nodeWithPosition = dagreGraph.node(node.id);
24 | node.position = {
25 | x: nodeWithPosition.x - nodeWidth / 2,
26 | y: nodeWithPosition.y - nodeHeight / 2,
27 | };
28 | });
29 |
30 | return { nodes, edges };
31 | };
32 |
33 | export default getLayoutedElements
--------------------------------------------------------------------------------
/client/src/components/Playground/ViewDrawer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import {
3 | Box,
4 | Drawer,
5 | Toolbar,
6 | List,
7 | Divider,
8 | ListItemButton,
9 | ListItemIcon,
10 | ListItemText,
11 | Collapse,
12 | } from "@mui/material";
13 | import {
14 | Inbox as InboxIcon,
15 | ExpandLess,
16 | ExpandMore,
17 | AccountTree as TreeIcon,
18 | DeviceHub as DiagramIcon,
19 | } from "@mui/icons-material";
20 | import { useAtom } from "jotai";
21 | import { userDataAtom, websitesAtom } from "../../state/Atoms";
22 | import { DrawerFrequencyProps } from "../../../types";
23 |
24 | const drawerWidth = 240;
25 |
26 | export default function PermanentDrawerLeft({
27 | onSelectView,
28 | onSelectWebsite,
29 | onSelectPage,
30 | }: DrawerFrequencyProps) {
31 | const [userData] = useAtom(userDataAtom);
32 | const [openMainItem, setOpenMainItem] = useState(null);
33 | const [openWebsite, setOpenWebsite] = useState(null);
34 | const [websitesList] = useAtom(websitesAtom);
35 |
36 | const handleMainItemClick = (item: string) => {
37 | setOpenMainItem(openMainItem === item ? null : item);
38 | };
39 |
40 | const handleWebsiteToggle = (website: string) => {
41 | setOpenWebsite(openWebsite === website ? null : website);
42 | };
43 |
44 | const handlePageClick = (website: string, page: string) => {
45 | onSelectWebsite(website);
46 | onSelectPage(page);
47 | onSelectView("frequency");
48 | };
49 |
50 | const handleViewChange = (view: string) => {
51 | onSelectView(view);
52 | };
53 |
54 | const pagesByWebsite: { [key: string]: string[] } = {};
55 |
56 | websitesList.forEach((website) => {
57 | const filteredUserData = userData.filter((item) => item.website_name === website);
58 | const pageUrls = filteredUserData.map((item) => item.page_url);
59 | const uniquePageUrlsSet = new Set(pageUrls);
60 | const uniquePageUrls = Array.from(uniquePageUrlsSet);
61 |
62 | pagesByWebsite[website] = uniquePageUrls;
63 | });
64 |
65 | return (
66 |
67 |
78 |
79 |
80 | handleMainItemClick("frequency")}>
81 |
82 |
83 |
84 |
85 | {openMainItem === "frequency" ? : }
86 |
87 |
92 |
93 | {websitesList.map((website) => (
94 |
95 | handleWebsiteToggle(website)}
97 | sx={{ pl: 4, mb: 2 }}
98 | >
99 |
100 |
101 |
102 |
103 | {openWebsite === website ? : }
104 |
105 |
110 |
111 | {pagesByWebsite[website].map((page_url) => (
112 | handlePageClick(website, page_url)}
116 | >
117 |
118 |
119 | ))}
120 |
121 |
122 |
123 | ))}
124 |
125 |
126 | handleViewChange("tree")}>
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 | );
142 | }
143 |
--------------------------------------------------------------------------------
/client/src/components/Playground/playgroundDisplay.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import FlowPlayground from "./FrequencyReactFlow/FlowPlayground.tsx";
3 | import OverallFlowPlayground from "./OverallReactFlow/OverallFlowPlayground.tsx";
4 | import PermanentDrawerLeft from "./ViewDrawer";
5 | import Navbar from "../Navbar/Navbar";
6 | import populateAtoms from "../../services/populateAtoms";
7 | import NavMobile from "../Navbar/NavMobile.tsx";
8 |
9 | const PlaygroundDisplay = () => {
10 | populateAtoms();
11 | const [selectedView, setSelectedView] = useState("tree");
12 | const [selectedWebsite, setSelectedWebsite] = useState("");
13 | const [selectedPage, setSelectedPage] = useState("");
14 |
15 | return (
16 |
17 |
18 |
19 |
24 |
25 | {selectedView === "frequency" && (
26 |
30 | )}
31 | {selectedView === "tree" && }
32 |
33 |
34 | );
35 | };
36 |
37 | export default PlaygroundDisplay;
38 |
--------------------------------------------------------------------------------
/client/src/components/Settings/ApiKeyDisplay.tsx:
--------------------------------------------------------------------------------
1 | import { Box, IconButton, Typography } from "@mui/material";
2 | import DeleteIcon from "@mui/icons-material/Delete";
3 | import RefreshIcon from "@mui/icons-material/Refresh";
4 | import ApiKeyDisplay from "./ApiKeyFormat";
5 |
6 | export const ApiKeySection = ({
7 | apiKey,
8 | onDelete,
9 | onRegenerate,
10 | }: {
11 | apiKey: string;
12 | onDelete: () => void;
13 | onRegenerate: () => void;
14 | }) => (
15 |
16 | Secret API key:
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 |
--------------------------------------------------------------------------------
/client/src/components/Settings/ApiKeyFormat.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { IconButton, TextField, Box } from '@mui/material';
3 | import { Visibility, VisibilityOff, FileCopy, Check } from '@mui/icons-material';
4 | import { CopyToClipboard } from 'react-copy-to-clipboard';
5 |
6 |
7 | const ApiKeyDisplay = ({ data }: { data: string }) => {
8 | const [showKey, setShowKey] = useState(false);
9 | const [copied, setCopied] = useState(false);
10 |
11 | const handleToggleVisibility = () => {
12 | setShowKey(!showKey);
13 | };
14 |
15 | const handleCopy = () => {
16 | setCopied(true);
17 | setTimeout(() => {
18 | setCopied(false);
19 | }, 1000);
20 | };
21 |
22 | return (
23 |
24 |
47 |
48 |
58 | {showKey ? : }
59 |
60 |
61 |
71 | {copied ? : }
72 |
73 |
74 |
75 |
76 | );
77 | };
78 |
79 | export default ApiKeyDisplay;
80 |
--------------------------------------------------------------------------------
/client/src/components/Settings/AwsBedrockConfig.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Typography, Button, TextField, CircularProgress, Alert, Select, MenuItem, InputLabel, FormControl } from "@mui/material";
2 | import { useState } from "react";
3 | import Navbar from "../Navbar/Navbar";
4 | import axios from "axios";
5 |
6 | const AwsBedrockConfig = () => {
7 | const [awsClientKey, setAwsClientKey] = useState("");
8 | const [awsSecretKey, setAwsSecretKey] = useState("");
9 | const [awsRegion, setAwsRegion] = useState("");
10 | const [loading, setLoading] = useState(false);
11 | const [alertMessage, setAlertMessage] = useState(null);
12 | const [alertType, setAlertType] = useState<"success" | "error" | null>(null);
13 |
14 | const handleSubmit = async (event: React.FormEvent) => {
15 | event.preventDefault();
16 | setLoading(true);
17 | setAlertMessage(null);
18 | if (!awsClientKey || !awsSecretKey || !awsRegion) {
19 | setAlertType("error");
20 | setAlertMessage("AWS Client Key, Secret Key, and Region are all required.");
21 | setLoading(false);
22 | return;
23 | }
24 |
25 | const token = localStorage.getItem("token");
26 |
27 | try {
28 | await axios.put(
29 | "/api/auth/awsCredentials",
30 | {
31 | awsClientKey,
32 | awsSecretKey,
33 | awsRegion,
34 | },
35 | {
36 | headers: {
37 | Authorization: `Bearer ${token}`,
38 | },
39 | }
40 | );
41 | setAwsClientKey("");
42 | setAwsSecretKey("");
43 | setAwsRegion("");
44 | setAlertType("success");
45 | setAlertMessage("AWS Credentials saved successfully!");
46 | } catch (error) {
47 | setAlertType("error");
48 | setAlertMessage("Error saving credentials. Please try again.");
49 | console.error("Error saving credentials:", error);
50 | } finally {
51 | setLoading(false);
52 | }
53 | };
54 |
55 | return (
56 |
57 |
58 |
59 |
60 | AWS Credentials
61 |
62 |
63 | {alertMessage && (
64 |
65 | {alertMessage}
66 |
67 | )}
68 |
69 |
70 | setAwsClientKey(e.target.value)}
77 | />
78 |
79 |
80 | setAwsSecretKey(e.target.value)}
87 | />
88 |
89 |
90 |
91 |
92 | AWS Region
93 | setAwsRegion(e.target.value as string)}
96 | label="AWS Region"
97 | >
98 | US East 1 (N. Virginia)
99 | US West 2 (Oregon)
100 |
101 |
102 |
103 |
104 |
111 | {loading ? : "Save Credentials"}
112 |
113 |
114 |
115 | );
116 | };
117 |
118 | export default AwsBedrockConfig;
119 |
--------------------------------------------------------------------------------
/client/src/components/Settings/Dialogs.tsx:
--------------------------------------------------------------------------------
1 | import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button } from "@mui/material";
2 |
3 | export const ApiKeyDialog = ({
4 | open,
5 | onClose,
6 | onConfirm,
7 | title,
8 | description,
9 | }: {
10 | open: boolean;
11 | onClose: () => void;
12 | onConfirm: () => void;
13 | title: string;
14 | description: string;
15 | }) => (
16 |
17 | {title}
18 |
19 | {description}
20 |
21 |
22 |
23 | No
24 |
25 |
26 | Yes
27 |
28 |
29 |
30 | );
31 |
--------------------------------------------------------------------------------
/client/src/components/Settings/UseApiKey.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { fetchApiKey, handleDeleteApiKey, handleRegenerateApiKey } from "../../services/apiKeyConfig";
3 |
4 | export const useApiKey = (token: string) => {
5 | const [apiKey, setApiKey] = useState("");
6 |
7 | useEffect(() => {
8 | const getApiKey = async () => {
9 | try {
10 | const key = await fetchApiKey(token);
11 | setApiKey(key);
12 | } catch (error) {
13 | console.error("Failed to fetch API key:", error);
14 | }
15 | };
16 | getApiKey();
17 | }, [token]);
18 |
19 | const deleteApiKey = async () => {
20 | try {
21 | const response = await handleDeleteApiKey(token);
22 | if (response) {
23 | setApiKey("");
24 | }
25 | } catch (error) {
26 | console.error("Error deleting API key: ", error);
27 | }
28 | };
29 |
30 | const regenerateApiKey = async () => {
31 | try {
32 | const response = await handleRegenerateApiKey(token);
33 | if (response) {
34 | setApiKey(response);
35 | return response;
36 | }
37 | } catch (error) {
38 | console.error("Error regenerating API key: ", error);
39 | }
40 | };
41 |
42 | return { apiKey, setApiKey, deleteApiKey, regenerateApiKey };
43 | };
44 |
--------------------------------------------------------------------------------
/client/src/components/Settings/WebsiteSelection.tsx:
--------------------------------------------------------------------------------
1 | import { Box, FormControl, Select, MenuItem, IconButton, Typography, CircularProgress } from "@mui/material";
2 | import DeleteIcon from "@mui/icons-material/Delete";
3 |
4 | export const WebsiteSelector = ({
5 | websites,
6 | selectedWebsite,
7 | loading,
8 | onSelectWebsite,
9 | onDelete,
10 | }: {
11 | websites: string[];
12 | selectedWebsite: string;
13 | loading: boolean;
14 | onSelectWebsite: (website: string) => void;
15 | onDelete: () => void;
16 | }) => (
17 |
18 |
19 |
20 | Select a Website to Delete
21 |
22 | onSelectWebsite(e.target.value)} displayEmpty>
23 |
24 | Select a website
25 |
26 | {websites.map((website, index) => (
27 |
28 | {website}
29 |
30 | ))}
31 |
32 |
33 |
34 |
35 | {loading ? : }
36 |
37 |
38 | );
39 |
--------------------------------------------------------------------------------
/client/src/components/User/ClickLog.module.css:
--------------------------------------------------------------------------------
1 | .ClickLog {
2 | height: 100%;
3 | min-width: fit-content;
4 | width: calc(100vw - 10rem);
5 |
6 |
7 | margin: 3rem;
8 | background: rgba(236, 236, 236, 0);
9 | display: flex;
10 | flex-direction: column;
11 | text-align: left;
12 | border: 2px solid rgba(0, 0, 0, 0.529);
13 | padding: 1rem;
14 | gap: 2rem;
15 | }
16 | /* .ClickLog h2 {
17 | padding: 1rem;
18 | } */
19 | table {
20 | border: none;
21 | }
22 |
23 | .ClickLog img {
24 | width: 40px;
25 | aspect-ratio: 1/1;
26 | border-radius: 100%;
27 | object-fit: contain;
28 | }
29 |
30 | .ClickLogItem {
31 | margin: 8px;
32 | padding: 1rem;
33 | }
34 |
35 |
36 |
37 | .ClickLogItems tr:nth-child(odd) {
38 | background: inherit;
39 | }
40 |
41 | .ClickLogItems tr:nth-child(even) {
42 | background: rgba(75,192,192,0.4);
43 | }
44 | .ClickLog button {
45 | border: none;
46 | }
47 | .ClickLogItems tr:hover {
48 | background: #f6735981;
49 | }
50 |
51 | .textRight {
52 | text-align: right;
53 | padding-right: 5px;
54 | }
55 | .noWrap {
56 | /* display: flex;
57 | flex-wrap: nowrap; */
58 | justify-content: center;
59 | white-space: nowrap;
60 | align-items: center;
61 | }
--------------------------------------------------------------------------------
/client/src/components/User/ClickLog.tsx:
--------------------------------------------------------------------------------
1 | import { useAtom } from 'jotai';
2 | import styles from './ClickLog.module.css';
3 |
4 | import ClickLogItem from './ClickLogItem';
5 | import { userDataAtom } from '../../state/Atoms';
6 | import { mapUserData } from '../../services/extractData';
7 |
8 | function ClickLog() {
9 | const [userData] = useAtom(userDataAtom);
10 | const mappedData:any = mapUserData(userData);
11 | // const [displayedItems, setDisplayedItems] = useState([])
12 | let displayedItems: any[] = []
13 |
14 | //date
15 | //time
16 | //interaction
17 | //element name
18 | //browser
19 | //os
20 | //website name
21 | function newItems() {
22 | for (let i = userData.length-1; i >=userData.length-10; i--)
23 | displayedItems.push( )
24 | }
25 | newItems();
26 |
27 | return (
28 |
29 |
30 |
Recent User Interacitons
31 |
32 |
33 |
34 |
35 |
36 | Date
37 | Time
38 | Interaction
39 | Element
40 | Browser
41 | OS
42 | Website
43 |
44 |
45 |
46 | {displayedItems}
47 |
48 |
49 |
50 | );
51 | }
52 |
53 | export default ClickLog;
54 |
--------------------------------------------------------------------------------
/client/src/components/User/ClickLogItem.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import styles from './ClickLog.module.css';
3 | import { ClickLogProps } from '../../../types';
4 |
5 | function ClickLogItem({item}:ClickLogProps) {
6 | const [date, setDate] = useState('');
7 | const [time, setTime] = useState('');
8 |
9 | useEffect(() => {
10 | const date = new Date(item.time);
11 | const localDate = date.toLocaleString().split(' ');
12 | setDate(localDate[0].slice(0, -1));
13 | setTime(localDate[1] + ' ' + localDate[2]);
14 | }, []);
15 |
16 |
17 |
18 | return (
19 |
20 | {date}
21 | {time}
22 | click
23 | {item.element+' '+ item.dataset_id}
24 | {item.user_browser.slice(0,7)}
25 | {item.user_os}
26 |
27 | {item.website}
28 |
29 |
30 | );
31 | }
32 |
33 | export default ClickLogItem;
34 |
--------------------------------------------------------------------------------
/client/src/components/User/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./UserView.module.css";
2 | import { activeWebsiteAtom } from "../../state/Atoms";
3 | import { useAtom } from "jotai";
4 | import ClickDataVisualization from "../ChartPages/AllUserData";
5 | import ClickDataVisualizationWebsite from "../ChartPages/WebsiteData";
6 | import { useParams } from "react-router-dom";
7 | import ClickLog from "./ClickLog";
8 |
9 |
10 |
11 | function Dashboard() {
12 | const [activeWebsite,setActiveWebsite] = useAtom(activeWebsiteAtom);
13 | const { id } = useParams();
14 | setActiveWebsite(id!)
15 |
16 | return (
17 |
18 |
19 |
20 |
21 | {activeWebsite === "overview"? (
22 |
23 | ) : (
24 |
25 | )}
26 |
27 |
28 | );
29 | }
30 |
31 | export default Dashboard;
32 |
--------------------------------------------------------------------------------
/client/src/components/User/DashboardDisplay.tsx:
--------------------------------------------------------------------------------
1 | import { useAtom } from "jotai";
2 | import populateAtoms from "../../services/populateAtoms";
3 | import { userDataAtom } from "../../state/Atoms";
4 | import Dashboard from "./Dashboard";
5 | import Documentation from "../Documentation/Documentation";
6 |
7 | function Final() {
8 | populateAtoms();
9 | const [data] = useAtom(userDataAtom);
10 |
11 | return (
12 |
13 | {data.length === 0 ? (
14 |
15 | ) : (
16 | <>
17 |
18 | >
19 |
20 | )}
21 |
22 |
23 | );
24 | }
25 |
26 | export default Final;
27 |
--------------------------------------------------------------------------------
/client/src/components/User/ForgotPassword.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import axios from 'axios';
3 | import { backendUrl } from '../../state/Atoms';
4 | import Navbar from '../Navbar/Navbar';
5 | import {
6 | Button,
7 | TextField,
8 | Card,
9 | CardContent,
10 | Typography,
11 | Box,
12 | } from '@mui/material';
13 |
14 | import {useNavigate } from 'react-router-dom';
15 |
16 | export default function ForgotPassword() {
17 | const [email, setEmail] = useState('');
18 | const [code, setCode] = useState('');
19 | const [newPassword, setNewPassword] = useState('');
20 | const [isCodeSent, setIsCodeSent] = useState(false);
21 | const navigate = useNavigate();
22 | const handleEmailSubmit = async (e: React.FormEvent) => {
23 | e.preventDefault();
24 | try {
25 | await axios.post(`${backendUrl}/api/auth/forgot-password`, { email });
26 | alert('Password reset code sent to your email.');
27 | setIsCodeSent(true);
28 | } catch (err: unknown) {
29 | const error = err as Error;
30 | console.error(error.message);
31 | alert('Error in sending password reset code.');
32 | }
33 | };
34 |
35 | const handleResetSubmit = async (e: React.FormEvent) => {
36 | e.preventDefault();
37 | try {
38 | await axios.post(`${backendUrl}/api/auth/confirm-password`, {
39 | email,
40 | code,
41 | newPassword,
42 | });
43 | alert('Password reset successful. You can now log in with your new password.');
44 | setIsCodeSent(false);
45 | setEmail('');
46 | setCode('');
47 | setNewPassword('');
48 | navigate('/login')
49 |
50 | } catch (err: unknown) {
51 | const error = err as Error;
52 | console.error(error.message);
53 | alert('Error in resetting password.');
54 | }
55 | };
56 |
57 | return (
58 |
59 |
60 |
61 |
62 |
63 | Forgot Password
64 |
65 |
66 | {isCodeSent
67 | ? "Enter the code you received and your new password"
68 | : "Enter your email to receive a reset code"}
69 |
70 | {!isCodeSent ? (
71 |
72 | setEmail(e.target.value)}
79 | required
80 | margin="normal"
81 | />
82 |
88 | Send Reset Code
89 |
90 |
91 | ) : (
92 |
93 | setCode(e.target.value)}
99 | required
100 | margin="normal"
101 | />
102 | setNewPassword(e.target.value)}
109 | required
110 | margin="normal"
111 | />
112 |
118 | Reset Password
119 |
120 |
121 | )}
122 |
123 |
124 |
125 | )
126 | }
--------------------------------------------------------------------------------
/client/src/components/User/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import styles from './UserView.module.css';
2 | import logo from '../../assets/icons/pie-chart.png';
3 | import dashboard from '../../assets/icons/dashboard.png';
4 | import globe from '../../assets/icons/globe.png'
5 | import { Link, NavLink } from 'react-router-dom';
6 | import { useAtom } from 'jotai';
7 | import { websitesAtom, activeWebsiteAtom } from '../../state/Atoms';
8 |
9 | function Sidebar() {
10 | const [websites] = useAtom(websitesAtom);
11 | const [, setSelectedWebsite] = useAtom(activeWebsiteAtom);
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
OS Analytics
19 |
20 |
21 |
22 |
23 |
24 | setSelectedWebsite('overview')}
28 | >
29 |
30 | {"Overview"}
31 |
32 |
33 | {websites.map((website, index) => (
34 |
35 | setSelectedWebsite(website)}
39 | >
40 |
41 | {website}
42 |
43 |
44 | ))}
45 |
46 |
47 |
48 | );
49 | }
50 |
51 | export default Sidebar;
52 |
--------------------------------------------------------------------------------
/client/src/components/User/UserView.module.css:
--------------------------------------------------------------------------------
1 | .dashboard {
2 | padding-top: 5rem;
3 | width: 100%;
4 | display: flex;
5 | flex-direction: column;
6 | flex-wrap: wrap;
7 | align-items: center;
8 | background: linear-gradient(135deg, #f7f7f7, #e0e0e0);
9 | }
10 |
11 | .sidebar {
12 | position: fixed;
13 | top: 0;
14 | left: 0;
15 | height: 100vh;
16 | background: transparent;
17 | width: 4rem;
18 | background: var(--blue-secondary);
19 | border-right: 1px solid var(--gray-border);
20 | z-index: 15;
21 | display: flex;
22 | flex-direction: column;
23 | justify-content: flex-start;
24 | align-items: start;
25 | transition: width 0.25s ease;
26 | overflow: hidden;
27 | }
28 |
29 | .sideLogo {
30 | width: 3rem;
31 | }
32 |
33 | .sideIcon {
34 | width: 2rem;
35 | }
36 | .logoBox {
37 | height: 4rem;
38 | display: flex;
39 | align-items: center;
40 | justify-content: start;
41 | gap: 1rem;
42 | margin-left: 0.5rem;
43 | overflow: hidden;
44 | transition: width 0.25s ease;
45 | text-wrap: nowrap;
46 | }
47 |
48 | .sidebarLinks {
49 | display: flex;
50 | flex-direction: column;
51 | justify-content: flex-start;
52 | align-items: start;
53 | margin: 1.5rem 0.5rem;
54 | }
55 |
56 | .sidebarLink {
57 | height: 3rem;
58 | display: flex;
59 | align-items: center;
60 | justify-content: start;
61 | gap: 1rem;
62 | border-radius: 6px;
63 | padding: 0 1rem;
64 | width: 3rem;
65 | overflow: hidden;
66 | transition: width 0.25s ease;
67 | }
68 |
69 | .sidebarSpan {
70 | padding-right: 2rem;
71 | }
72 |
73 | .sidebar:hover {
74 | width: 14.5rem;
75 | }
76 |
77 | .sidebar:hover .sidebarLink {
78 | width: 13.5rem;
79 | }
80 |
81 | a.active {
82 | background: rgba(255, 255, 255, 0.1);
83 | }
84 |
85 | .timeframe {
86 | width: 10.5rem;
87 | text-align: left;
88 | /* border: 1px solid var(--blue-secondary); */
89 | }
90 |
91 | .websiteDropdown {
92 | width: 10.5rem;
93 | text-align: left;
94 | display: none;
95 | }
96 |
97 | @media only screen and (max-width: 850px) {
98 | .sidebar {
99 | display: none;
100 | }
101 | .websiteDropdown {
102 | display: flex;
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/client/src/components/User/UserView.tsx:
--------------------------------------------------------------------------------
1 | import NavbarDashboard from "../Navbar/NavbarDashboard"
2 | import NavMobile from "../Navbar/NavMobile"
3 | import Sidebar from "./Sidebar"
4 | import DashBoardDisplay from "./DashboardDisplay"
5 | function UserView(){
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 | )
14 | }
15 |
16 | export default UserView
17 |
--------------------------------------------------------------------------------
/client/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 |
4 | import App from './App.tsx'
5 | import './main.css'
6 |
7 | createRoot(document.getElementById('root')!).render(
8 |
9 |
10 |
11 |
12 | )
13 |
--------------------------------------------------------------------------------
/client/src/services/apiKeyConfig.tsx:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import {backendUrl} from '../state/Atoms';
3 |
4 | export const fetchApiKey = async (token: string) => {
5 | try {
6 | const response = await axios.get(`${backendUrl}/api/auth/getApiKey`, {
7 | headers: {
8 | Authorization: `Bearer ${token}`,
9 | },
10 | });
11 | return response.data.apiKey;
12 | } catch (error) {
13 | console.error("Error fetching API key:", error);
14 | throw error;
15 | }
16 | };
17 |
18 | export const handleDeleteApiKey = async (token: string) => {
19 |
20 | try {
21 | const response = await axios.delete(`${backendUrl}/api/auth/apiKey`, {
22 | headers: {
23 | 'Content-Type': 'application/json',
24 | 'Authorization': `Bearer ${token}`,
25 | },
26 | });
27 |
28 | if (response.status === 200) {
29 | return response.status === 200
30 |
31 | } else {
32 | console.error("Failed to delete API key");
33 | }
34 | } catch (error) {
35 | console.error("Error deleting API key: ", error);
36 | }
37 | };
38 |
39 | export const handleRegenerateApiKey = async (token: string) => {
40 | try {
41 | const response = await axios.put(`${backendUrl}/api/auth/apiKey`, {},
42 | {
43 | headers: {
44 | 'Content-Type': 'application/json',
45 | 'Authorization': `Bearer ${token}`,
46 | },
47 | }
48 | );
49 | if (response.status === 200) {
50 | const {apiKey} = response.data;
51 |
52 | return apiKey;
53 | } else {
54 | console.error("Failed to regenerate API key");
55 | }
56 | } catch (error) {
57 | console.error("Error regenerating API key: ", error);
58 | }
59 | };
60 |
--------------------------------------------------------------------------------
/client/src/services/authConfig.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import {backendUrl} from '../state/Atoms';
3 |
4 | export const handleLogout = async () => {
5 | window.location.href = 'http://os-analytics.com.s3-website-us-west-1.amazonaws.com/';
6 |
7 |
8 | localStorage.removeItem('token');
9 |
10 | };
11 |
12 | export const handleSession = async () => {
13 | const token = localStorage.getItem('token');
14 | try {
15 | const response = await axios.get(`${backendUrl}/api/auth/activeUser`, {
16 | headers: {
17 | Authorization: `Bearer ${token}`
18 | }
19 | });
20 |
21 | if (response.status === 200) {
22 | return response.data.email;
23 | } else {
24 | return "";
25 | }
26 | } catch (err) {
27 | localStorage.removeItem('token');
28 |
29 | return "";
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/client/src/services/deleteDataApi.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { backendUrl } from "../state/Atoms";
3 |
4 | export const deleteWebsite = async (selectedWebsite: string, token: string) => {
5 | try {
6 | const response = await axios.delete(`${backendUrl}/api/data/delete-website`, {
7 | headers: {
8 | Authorization: `Bearer ${token}`,
9 | },
10 | data: { website_name: selectedWebsite },
11 | });
12 |
13 | return response.status === 200;
14 | } catch (error) {
15 | console.error("Error deleting website:", error);
16 | throw error;
17 | }
18 | };
19 | export const deleteAccount = async (token: string) => {
20 | try {
21 | const response = await axios.delete(`${backendUrl}/api/auth/delete-account`, {
22 | headers: {
23 | Authorization: `Bearer ${token}`,
24 | },
25 | });
26 |
27 | return response.status === 200;
28 | } catch (error) {
29 | console.error("Error deleting account:", error);
30 | throw error;
31 | }
32 | };
--------------------------------------------------------------------------------
/client/src/services/extractData.ts:
--------------------------------------------------------------------------------
1 | import { QueryData } from "../../types";
2 |
3 | export const extractBrowserAndOS = (userAgent: string) => {
4 | let browser = "Unknown Browser";
5 | let os = "Unknown OS";
6 |
7 | if (userAgent.includes("Chrome")) {
8 | browser = "Chrome";
9 | } else if (userAgent.includes("Firefox")) {
10 | browser = "Firefox";
11 | } else if (userAgent.includes("Safari") && !userAgent.includes("Chrome")) {
12 | browser = "Safari";
13 | } else if (userAgent.includes("Edge")) {
14 | browser = "Edge";
15 | }
16 |
17 | if (userAgent.includes("Windows NT 10.0")) {
18 | os = "Windows 10";
19 | } else if (userAgent.includes("Windows NT 6.1")) {
20 | os = "Windows 7";
21 | } else if (userAgent.includes("Mac OS X") && !userAgent.includes("Mobile")) {
22 | os = "macOS";
23 | } else if (userAgent.includes("iPhone") || userAgent.includes("iPad")) {
24 | os = "iOS";
25 | } else if (userAgent.includes("Android")) {
26 | os = "Android";
27 | } else if (userAgent.includes("Linux") && !userAgent.includes("Android")) {
28 | os = "Linux";
29 | }
30 | return { browser, os };
31 | };
32 |
33 | export const mapUserData = (userData: QueryData[]) => {
34 | return userData.map((query) => {
35 | const { browser, os } = extractBrowserAndOS(query.user_browser);
36 |
37 | return {
38 | element: query.element,
39 | dataset_id: query.dataset_id,
40 | x_coord: query.x_coord,
41 | y_coord: query.y_coord,
42 | time: query.created_at,
43 | user_browser: browser,
44 | website: query.website_name,
45 | user_os: os,
46 | page_url: query.page_url,
47 | };
48 | });
49 | };
50 |
--------------------------------------------------------------------------------
/client/src/services/filterDataByReferralTimeFrame.tsx:
--------------------------------------------------------------------------------
1 | import { referralData } from "../../types";
2 | export const filterReferralDataByTimeFrame = (
3 | data: referralData[],
4 | timeFrame: string
5 | ) => {
6 | const filteredData = [];
7 | const now = new Date();
8 |
9 | for (let i = 0; i < data.length; i++) {
10 | const item = data[i];
11 | const clickTime = new Date(item.created_at || 0);
12 |
13 | switch (timeFrame) {
14 | case "1 day":
15 | if (now.getTime() - clickTime.getTime() <= 24 * 60 * 60 * 1000) {
16 | filteredData.push(item);
17 | }
18 | break;
19 | case "1 week":
20 | if (now.getTime() - clickTime.getTime() <= 7 * 24 * 60 * 60 * 1000) {
21 | filteredData.push(item);
22 | }
23 | break;
24 | case "1 month":
25 | if (now.getTime() - clickTime.getTime() <= 30 * 24 * 60 * 60 * 1000) {
26 | filteredData.push(item);
27 | }
28 |
29 | break;
30 | case "1 year":
31 | if (now.getFullYear() === clickTime.getFullYear()) {
32 | filteredData.push(item);
33 | }
34 | break;
35 | case "5 years":
36 | if (now.getFullYear() - clickTime.getFullYear() <= 5) {
37 | filteredData.push(item);
38 | }
39 | break;
40 | case "allTime":
41 | default:
42 | filteredData.push(item);
43 | break;
44 | }
45 | }
46 |
47 | return filteredData;
48 | };
49 |
--------------------------------------------------------------------------------
/client/src/services/filterDataByTimeFrame .ts:
--------------------------------------------------------------------------------
1 | import { QueryData } from "../../types";
2 | export const filterDataByTimeFrame = (data: QueryData[], timeFrame: string) => {
3 | const filteredData = [];
4 | const now = new Date();
5 |
6 | for (let i = 0; i < data.length; i++) {
7 | const item = data[i];
8 | const clickTime = new Date(item.time || 0);
9 |
10 | switch (timeFrame) {
11 | case "1 day":
12 | if (now.getTime() - clickTime.getTime() <= 24 * 60 * 60 * 1000) {
13 | filteredData.push(item);
14 | }
15 | break;
16 | case "1 week":
17 | if (now.getTime() - clickTime.getTime() <= 7 * 24 * 60 * 60 * 1000) {
18 | filteredData.push(item);
19 | }
20 | break;
21 | case "1 month":
22 | if (now.getTime() - clickTime.getTime() <= 30 * 24 * 60 * 60 * 1000) {
23 | filteredData.push(item);
24 | }
25 | break;
26 | case "1 year":
27 | if (now.getFullYear() === clickTime.getFullYear()) {
28 | filteredData.push(item);
29 | }
30 | break;
31 | case "5 years":
32 | if (now.getFullYear() - clickTime.getFullYear() <= 5) {
33 | filteredData.push(item);
34 | }
35 | break;
36 | case "allTime":
37 | default:
38 | filteredData.push(item);
39 | break;
40 | }
41 | }
42 |
43 | return filteredData;
44 | };
45 |
--------------------------------------------------------------------------------
/client/src/services/populateAtoms.ts:
--------------------------------------------------------------------------------
1 | import {
2 | userDataAtom,
3 | userReferralDataAtom,
4 | websitesAtom,
5 | backendUrl,
6 | } from "../state/Atoms";
7 | import { useAtom } from "jotai";
8 | import axios from "axios";
9 | import { useEffect } from "react";
10 |
11 | async function populateAtoms() {
12 | const [, setUserData] = useAtom(userDataAtom);
13 | const [, setUserReferralData] = useAtom(userReferralDataAtom);
14 | const [, setWebsites] = useAtom(websitesAtom);
15 |
16 | const token = localStorage.getItem("token");
17 |
18 | useEffect(() => {
19 | const fetchData = async () => {
20 | try {
21 | const userDataResponse = await axios.get(`${backendUrl}/api/data`, {
22 | headers: {
23 | Authorization: `Bearer ${token}`,
24 | },
25 | });
26 |
27 | setUserData(userDataResponse.data);
28 |
29 | const websiteList: Set = new Set(
30 | userDataResponse.data.map((el: { website_name: string }) => el.website_name)
31 | );
32 | setWebsites(Array.from(websiteList));
33 |
34 | const userReferralDataResponse = await axios.get(`${backendUrl}/api/data/referral`, {
35 | headers: {
36 | Authorization: `Bearer ${token}`,
37 | },
38 | });
39 |
40 | setUserReferralData(userReferralDataResponse.data);
41 | } catch (error) {
42 | console.error("Error fetching data:", error);
43 | }
44 | };
45 |
46 | fetchData();
47 | }, [setUserData, setUserReferralData, setWebsites, token]);
48 |
49 | }
50 |
51 | export default populateAtoms;
52 |
--------------------------------------------------------------------------------
/client/src/state/Atoms.tsx:
--------------------------------------------------------------------------------
1 | import { atom } from "jotai";
2 | import { QueryData, referralData } from "../../types";
3 |
4 | // const userStore = createStore();
5 | // IF RUNNING ON LOCAL HOST, REPLACE backendURL value with empty string.
6 | //http://ec2-18-144-89-57.us-west-1.compute.amazonaws.com
7 |
8 | export const backendUrl: string = "http://ec2-18-144-89-57.us-west-1.compute.amazonaws.com:8080"; // backend URI
9 | export const activeUserAtom = atom("");
10 | export const activeNavAtom = atom(false);
11 |
12 | export const loadingAtom = atom(false);
13 |
14 | // User's Click Data
15 | export const userDataAtom = atom([]);
16 | export const userReferralDataAtom = atom([]);
17 | export const websitesAtom = atom([]);
18 | export const activeWebsiteAtom = atom("overview");
19 |
20 | export const websiteDataAtom = atom((get) => {
21 | //references the active website atom
22 | const activeWebsite = get(activeWebsiteAtom);
23 | //references the complete data set and filters according to the active website atom
24 | return get(userDataAtom).filter((el) => el.website_name === activeWebsite);
25 | });
26 |
27 | export const websiteReferralDataAtom = atom((get) => {
28 | const activeWebsite = get(activeWebsiteAtom);
29 | return get(userReferralDataAtom).filter(
30 | (el) => el.website_name === activeWebsite
31 | );
32 | });
33 | export const timeFrameAtom = atom("1 day");
34 |
--------------------------------------------------------------------------------
/client/src/types-packages/three.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'vanta/dist/vanta.net.min' {
2 | const NET: any;
3 | export default NET;
4 | }
--------------------------------------------------------------------------------
/client/src/types-packages/vanta.d.ts:
--------------------------------------------------------------------------------
1 | declare module "vanta/dist/vanta.net.min" {
2 | const NET: any;
3 | export default NET;
4 | }
5 |
6 | declare module "vanta/dist/vanta.globe.min" {
7 | const GLOBE: any;
8 | export default GLOBE;
9 | }
10 |
--------------------------------------------------------------------------------
/client/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/client/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "noImplicitAny": true
23 | },
24 | "include": ["src/**/*"]
25 | }
26 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/client/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "lib": ["ES2023"],
5 | "module": "ESNext",
6 | "skipLibCheck": true,
7 |
8 | /* Bundler mode */
9 | "moduleResolution": "bundler",
10 | "allowImportingTsExtensions": true,
11 | "isolatedModules": true,
12 | "moduleDetection": "force",
13 | "noEmit": true,
14 |
15 | /* Linting */
16 | "strict": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noFallthroughCasesInSwitch": true
20 | },
21 | "include": ["vite.config.ts"]
22 | }
23 |
--------------------------------------------------------------------------------
/client/types.tsx:
--------------------------------------------------------------------------------
1 | export type BarChartProps = {
2 | data: QueryData[];
3 | keyword: string;
4 | };
5 | export type ClickLogProps = {
6 | item: {
7 | element: string;
8 | dataset_id: string;
9 | x_coord: number;
10 | y_coord: number;
11 | time: string;
12 | user_browser: string;
13 | website: string;
14 | user_os: string;
15 | page_url: string;
16 | }
17 |
18 | }
19 |
20 | export type RadarChartProps = {
21 | data: QueryData[];
22 | keyword: string;
23 | keywordTwo: string;
24 | };
25 | export type StackedBarChart = {
26 | data: QueryData[];
27 | keyword: string;
28 | keywordTwo: string;
29 | };
30 |
31 | export type NoKeywordChart = {
32 | data: QueryData[];
33 | };
34 |
35 | export type WebsiteCounts = {
36 | data: QueryData[];
37 | };
38 |
39 | export type PieChartsProps = {
40 | data: QueryData[];
41 | keyword: string;
42 | keywordTwo: string;
43 | };
44 |
45 | export type ChartDownloadProps = {
46 | chartRef: { current: { toBase64Image: () => string } | null };
47 | fileName?: string;
48 | }
49 |
50 | export type QueryData = {
51 | element: string;
52 | dataset_id: string;
53 | time?: string;
54 | x_coord: number;
55 | y_coord: number;
56 | user_browser: string;
57 | page_url: string;
58 | created_at?: string;
59 | website_name?: string;
60 | [key: string]: any; //this could be anything its a keyword that the userdefines
61 | };
62 |
63 | export type referralData = {
64 | website_name?: string;
65 | referrer: string;
66 | created_at?: string;
67 | };
68 |
69 | export type referralBarChartProps = {
70 | data: referralData[];
71 | };
72 |
73 | export type DrawerFrequencyProps = {
74 | onSelectView: (view: string) => void;
75 | onSelectWebsite: (website: string) => void;
76 | onSelectPage: (page: string) => void;
77 | };
78 |
79 | export type FrequencyProps = {
80 | selectedWebsite: string;
81 | selectedPage: string;
82 | };
83 |
84 | export type AggregatedData = {
85 | [key: string]: {
86 | dataset_id: string;
87 | x_coord: number;
88 | y_coord: number;
89 | count: number;
90 | };
91 | };
92 |
93 | export type InteractionData = {
94 | dataset_id: string;
95 | website_name: string;
96 | page_url: string;
97 | x_coord: string;
98 | y_coord: string;
99 | };
100 |
--------------------------------------------------------------------------------
/client/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | port: 3000,
9 | proxy: {
10 | '/api': 'http://localhost:8080',
11 | },
12 | },
13 | build: { // in order to chunk large chunks during build, chunks will be lost otherwise
14 | rollupOptions: {
15 | output:{
16 | manualChunks(id) {
17 | if (id.includes('node_modules')) {
18 | return id.toString().split('node_modules/')[1].split('/')[0].toString();
19 | }
20 | }
21 | }
22 | }
23 | }
24 | })
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "activitytracker.io-backend",
3 | "version": "1.0.0",
4 | "description": "activitytracker",
5 | "main": "server.js",
6 | "scripts": {
7 | "dev": "concurrently \"npm run start\" \"npm run client\"",
8 | "start": "cd server && npm start",
9 | "build": "npm --prefix client run build",
10 | "client": "npm --prefix client run dev"
11 | },
12 | "author": "",
13 | "license": "ISC",
14 | "dependencies": {
15 | "concurrently": "^8.2.2",
16 | "react-icons": "^5.3.0"
17 | },
18 | "devDependencies": {
19 | "@types/cookie-parser": "^1.4.7",
20 | "@types/passport": "^1.0.16",
21 | "@types/passport-google-oauth20": "^2.0.16"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .env
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | # Activity-Tracker.io-Backend
2 | We provide solutions for developers to ship out websites to clients with dashboards that contain information about micro-interactions (user clicks and mouse movement) on their websites. Team members: Peter Larcheveque, Eric DiMarzio, David Naymon, Saw Naing
3 |
--------------------------------------------------------------------------------
/server/controllers/aiController.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import { pool } from "../models/db";
3 | const {
4 | BedrockRuntimeClient,
5 | InvokeModelCommand,
6 | } = require("@aws-sdk/client-bedrock-runtime");
7 | import { ClickData, OpenAIResponse } from "../types";
8 | import { decrypt } from "../middleware/awsEncryption";
9 |
10 | const aiController = {
11 | async getDataBedrock(req: Request, res: Response, next: NextFunction) {
12 | const id = res.locals.userId;
13 | let { website, timeFrame } = req.body;
14 | if (website == "overview") {
15 | website = "";
16 | }
17 |
18 | let query: string,
19 | queryParams: Array = [id];
20 |
21 | if (website) {
22 | query = `
23 | SELECT
24 | "page_url",
25 | COUNT(*) AS total_clicks,
26 | AVG(CAST("x_coord" AS FLOAT)) AS avg_x_coord,
27 | AVG(CAST("y_coord" AS FLOAT)) AS avg_y_coord
28 | FROM
29 | "clickTable"
30 | WHERE
31 | "user_id" = $1
32 | AND "website_name" = $2
33 | ${
34 | timeFrame !== "allTime"
35 | ? `AND "created_at" >= NOW() - INTERVAL '${timeFrame}'`
36 | : ""
37 | }
38 | GROUP BY
39 | "page_url"
40 | ORDER BY
41 | "page_url";
42 | `;
43 | queryParams.push(website);
44 | } else {
45 | query = `
46 | SELECT
47 | "website_name",
48 | COUNT(*) AS total_clicks
49 | FROM
50 | "clickTable"
51 | WHERE
52 | "user_id" = $1
53 | ${
54 | timeFrame !== "allTime"
55 | ? `AND "created_at" >= NOW() - INTERVAL '${timeFrame}'`
56 | : ""
57 | }
58 | GROUP BY
59 | "website_name"
60 | ORDER BY
61 | total_clicks DESC;
62 | `;
63 | }
64 | try {
65 | const awsCredsQuery = `
66 | SELECT "AWS_ACCESS_KEY", "AWS_SECRET_KEY", "AWS_REGION"
67 | FROM "userTable"
68 | WHERE "cognito_id" = $1
69 | `;
70 | const awsCredsResponse = await pool.query(awsCredsQuery, [id]);
71 |
72 | if (awsCredsResponse.rows.length === 0) {
73 | throw new Error("AWS credentials not found for the given cognito_id");
74 | }
75 |
76 | const encryptedClientKey = JSON.parse(
77 | awsCredsResponse.rows[0].AWS_ACCESS_KEY
78 | );
79 | const encryptedSecretKey = JSON.parse(
80 | awsCredsResponse.rows[0].AWS_SECRET_KEY
81 | );
82 | const AWS_REGION = awsCredsResponse.rows[0].AWS_REGION;
83 |
84 | const AWS_ACCESS_KEY = decrypt(encryptedClientKey);
85 | const AWS_SECRET_KEY = decrypt(encryptedSecretKey);
86 |
87 | const client = new BedrockRuntimeClient({
88 | region: AWS_REGION,
89 | credentials: {
90 | accessKeyId: AWS_ACCESS_KEY,
91 | secretAccessKey: AWS_SECRET_KEY,
92 | },
93 | });
94 | const response = await pool.query(query, queryParams);
95 | if (response.rows.length === 0) {
96 | return res.status(404).json({
97 | message: "Please gather some data before generating an AI response.",
98 | });
99 | }
100 |
101 | const relevantData = response.rows.map((row: ClickData) => {
102 | if (website) {
103 | return {
104 | page_url: row.page_url,
105 | total_clicks: row.total_clicks,
106 | avg_x_coord: row.avg_x_coord,
107 | avg_y_coord: row.avg_y_coord,
108 | };
109 | } else {
110 | return {
111 | website_name: row.website_name,
112 | total_clicks: row.total_clicks,
113 | };
114 | }
115 | });
116 |
117 | let promptText = "";
118 |
119 | if (website) {
120 | promptText = `Provide a summary of user interactions with the website, including:
121 |
122 | Page URLs and their total number of clicks.
123 | The average x and y coordinates of clicks, normalized between 0 and 1 to account for screen size dynamically.
124 | Based on this data, generate:
125 |
126 | An overall report of the user interactions.
127 | An analysis of the x and y coordinates, detailing where the majority of clicks occurred on the screen relative to screen size.
128 | Please present the findings in a numbered list format.
129 | `;
130 | } else {
131 | promptText = `
132 | Here is the summarized data of user interactions:
133 | - Website names and the total number of clicks.
134 | Please provide an overall report and summary of this data.
135 | Please keep this in numbered list format.
136 | `;
137 | }
138 |
139 | const requestBody = JSON.stringify({
140 | inputText: JSON.stringify(relevantData) + promptText,
141 | textGenerationConfig: {
142 | maxTokenCount: 2000,
143 | },
144 | });
145 | const params = {
146 | modelId: "amazon.titan-text-premier-v1:0",
147 | contentType: "application/json",
148 | accept: "application/json",
149 | body: requestBody,
150 | };
151 |
152 | const command = new InvokeModelCommand(params);
153 | const summarized = await client.send(command);
154 | const result = JSON.parse(Buffer.from(summarized.body).toString("utf-8"));
155 | res.json(result);
156 | } catch (err: unknown) {
157 | const error = err as Error;
158 | res.status(500).json({
159 | message:
160 | "Invalid AWS credentials or server error, please try again later.",
161 | });
162 | return next({
163 | message: "Error in getDataBedrock: " + error.message,
164 | log: error,
165 | });
166 | }
167 | },
168 | };
169 |
170 | export default aiController;
171 |
--------------------------------------------------------------------------------
/server/controllers/clickController.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import { pool } from "../models/db";
3 |
4 | const clickDataController = {
5 | async storeClickData (req: Request, res: Response){
6 |
7 | //get click data back as a body
8 | const { websiteName, x_coord, y_coord, element, activityId, userAgent, platform, pageUrl} = req.body;
9 | const userId = res.locals.user;
10 | if (!userId) {
11 | res
12 | .status(400)
13 | .json({ error: "User information is missing from the request" });
14 | return;
15 | }
16 | //insert query to store click data in clickTable
17 | try {
18 | await pool.query(
19 | `
20 | INSERT INTO "clickTable" (user_id, website_name, element, dataset_id, x_coord, y_coord, user_browser,user_os,page_url)
21 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
22 | [userId, websiteName, element,activityId, x_coord, y_coord, userAgent, platform, pageUrl]
23 | );
24 |
25 | res.status(201).json({ message: "Click data stored successfully" });
26 | } catch (error) {
27 | console.error("Error storing click data:", error);
28 | res.status(500).json({ error: "Failed to store click data" });
29 | }
30 | },
31 |
32 | async storeVisitData(req: Request, res: Response) {
33 |
34 | const { websiteName, referrer } = req.body;
35 | const userId = res.locals.user;
36 |
37 | if (!userId) {
38 | res
39 | .status(400)
40 | .json({ error: "User information is missing from the request" });
41 | return;
42 | }
43 | try {
44 | await pool.query(
45 | `
46 | INSERT INTO "referrerTable" (user_id, website_name, referrer)
47 | VALUES ($1, $2, $3)`,
48 | [userId, websiteName, referrer]
49 | );
50 |
51 | res.status(201).json({ message: "Visit data stored successfully" });
52 | } catch (error) {
53 | console.error("Error storing visit data:", error);
54 | res.status(500).json({ error: "Failed to store visit data" });
55 | }
56 | },
57 | };
58 |
59 | export default clickDataController;
60 |
--------------------------------------------------------------------------------
/server/controllers/dataController.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import { pool } from "../models/db";
3 |
4 | const dataController = {
5 | //get everything from all website, URL looks like http://yourdomain.com/api/data
6 | async getAllUserData(req: Request, res: Response, next: NextFunction) {
7 | try {
8 | const id = res.locals.userId;
9 | //user_id, element, element_name, dataset_id, x_coord, y_coord, user_browser, user_os, page_url
10 | const response = await pool.query(
11 | `SELECT *
12 | FROM "clickTable"
13 | WHERE user_id = $1
14 | ORDER BY created_at ASC;`,
15 | [id]
16 | );
17 |
18 | if (response.rows.length > 0) {
19 | res.status(200).json(response.rows);
20 | } else {
21 | res.status(404).json({ message: "No data" });
22 | }
23 | } catch (err) {
24 | const error = err as Error;
25 | return next({
26 | message: "Error in getAllUserData: " + error.message,
27 | log: err,
28 | });
29 | }
30 | },
31 |
32 | async getAllreferralData(req: Request, res: Response, next: NextFunction) {
33 | try {
34 | const id = res.locals.userId;
35 | const response = await pool.query(
36 | `SELECT *
37 | FROM "referrerTable"
38 | WHERE user_id = $1`,
39 | [id]
40 | );
41 | if (response.rows.length > 0) {
42 | res.status(200).json(response.rows);
43 | } else {
44 | res.status(404).json({ message: "No data" });
45 | }
46 | } catch (err) {
47 | const error = err as Error;
48 | return next({
49 | message: "Error in getAllreferralData: " + error.message,
50 | log: err,
51 | });
52 | }
53 | },
54 | async deleteWebsite(req: Request, res: Response, next: NextFunction) {
55 | const { website_name } = req.body;
56 | const userId = res.locals.userId;
57 |
58 | try {
59 | const deleteClicks = await pool.query(
60 | `DELETE FROM "clickTable" WHERE website_name = $1 AND user_id = $2`,
61 | [website_name, userId]
62 | );
63 | const deleteReferrals = await pool.query(
64 | `DELETE FROM "referrerTable" WHERE website_name = $1 AND user_id = $2`,
65 | [website_name, userId]
66 | );
67 |
68 | if (deleteClicks.rowCount > 0 || deleteReferrals.rowCount > 0) {
69 | res.status(200).json({ message: "Website data deleted successfully" });
70 | } else {
71 | res.status(404).json({ message: "No data found for this website" });
72 | }
73 | } catch (err) {
74 | const error = err as Error;
75 | return next({
76 | message: "Error in deleteWebsite: " + error.message,
77 | log: err,
78 | });
79 | }
80 | },
81 | async getWebsiteData(req: Request, res: Response, next: NextFunction) {
82 | try {
83 | const website: string = req.params.id;
84 | const id: string = res.locals.userId;
85 | const response = await pool.query(
86 | `SELECT * FROM "clickTable"
87 | WHERE user_id = $1
88 | AND website_name = $2
89 | `,
90 | [id, website]
91 | );
92 | if (response.rows.length > 0) {
93 | res.status(200).json(response.rows);
94 | } else {
95 | res.status(404).json({ message: "No data" });
96 | }
97 | } catch (err) {
98 | const error = err as Error;
99 | return next({
100 | message: "Error in getWebsiteData:" + error.message,
101 | log: err,
102 | });
103 | }
104 | },
105 |
106 | async getAllUserWebsites(req: Request, res: Response, next: NextFunction) {
107 | try {
108 | const id = res.locals.userId;
109 |
110 | const response = await pool.query(
111 | `SELECT DISTINCT website_name
112 | FROM "clickTable"
113 | WHERE user_id = $1`,
114 | [id]
115 | );
116 |
117 | if (response.rows.length > 0) {
118 | res.status(200).json(response.rows);
119 | } else {
120 | res.status(404).json({ message: "No data" });
121 | }
122 | } catch (err) {
123 | const error = err as Error;
124 | return next({
125 | message: "Error in getAllUserWebsites: " + error.message,
126 | log: err,
127 | });
128 | }
129 | },
130 | };
131 |
132 | export default dataController;
133 |
--------------------------------------------------------------------------------
/server/controllers/oauthRequestRoutes.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response, NextFunction } from "express";
2 | const router = express.Router();
3 | import dotenv from 'dotenv';
4 | dotenv.config();
5 | const{Oauth2Client} = require('google-auth-library');
6 |
7 | router.post('/googleoauth', async function(req,res,next){
8 | res.header('Access-Control-Allow-Origin','http:localhost:3000');
9 | res.header('Referrer-Policy', 'no-Referrer-when-downgrade');
10 |
11 | const redirectURL = 'http://localhost:3000/';
12 |
13 | const oAuth2Client = new Oauth2Client (
14 | process.env.GOOGLE_CLIENT_ID,
15 | process.env.GOOGLE_CLIENT_SECRET,
16 | redirectURL
17 | );
18 | const autuhorizeURL = oAuth2Client.generateAuthUrl({
19 | access_type: 'offline',
20 | scope: ['https://www.googlepais.com/auth/userinfo.profile openid'],
21 | prompt: 'consent'
22 |
23 | });
24 | res.json({url:autuhorizeURL});
25 | });
26 | export default router;
--------------------------------------------------------------------------------
/server/controllers/oauthRoutes.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response, NextFunction } from "express";
2 | const router = express.Router();
3 | import dotenv from 'dotenv';
4 | dotenv.config();
5 | const{Oauth2Client} = require('google-auth-library')
6 |
7 |
--------------------------------------------------------------------------------
/server/controllers/puppeteerController.ts:
--------------------------------------------------------------------------------
1 | import puppeteer from "puppeteer"
2 | import { Request, Response, NextFunction } from "express"
3 |
4 |
5 | const puppeteerController = {
6 | // take screenshot of user's website
7 | async takeScreenshot(req: Request, res: Response, next: NextFunction) {
8 | try{
9 | const homepage_url:any = req.query.url // URL is given by the "user" (your client-side application)
10 |
11 | // launch headless browser, navigate to webpage, and take screenshot
12 | const browser = await puppeteer.launch({headless: true, args: ['--no-sandbox']});
13 | const page:any = await browser.newPage();
14 |
15 | // Set the viewport (width, height, and optional device scale factor)
16 | await page.setViewport({
17 | width: 1280, // Set the desired width
18 | height: 720, // Set the desired height
19 | deviceScaleFactor: 1 // Adjust for HiDPI screens, default is 1
20 | });
21 |
22 | // No timeout when generating
23 | page.setDefaultNavigationTimeout(0);
24 |
25 | await page.goto(homepage_url);
26 | const screenshotBuffer = await page.screenshot();
27 | res.locals.screenshotBuffer = await screenshotBuffer;
28 | await browser.close();
29 | return next();
30 | }
31 | catch(err) {
32 | const error = err as Error;
33 | return next({
34 | message: "Error in takeScreenshot: " + error.message,
35 | log: err,
36 | });
37 | }
38 | // const query:any = req.query.url
39 | // const browser = await puppeteer.launch();
40 | // const page = await browser.newPage();
41 | // await page.goto(query); // URL is given by the "user" (your client-side application)
42 | // const screenshotBuffer = await page.screenshot();
43 |
44 | // // Respond with the image
45 | // res.writeHead(200, {
46 | // 'Content-Type': 'image/png',
47 | // 'Content-Length': screenshotBuffer.length
48 | // });
49 | // res.end(screenshotBuffer);
50 |
51 | // await browser.close();
52 | }
53 | }
54 |
55 | export default puppeteerController;
--------------------------------------------------------------------------------
/server/middleware/addToDB.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import { pool } from "../models/db";
3 |
4 | const addUserToDatabase = async (req: Request, res: Response, next: NextFunction) => {
5 | const { email } = req.body;
6 | const cognito_id = res.locals.cognito_Id;
7 | try {
8 | const existingUser = await pool.query(
9 | 'SELECT * FROM "userTable" WHERE email = $1',
10 | [email]
11 | );
12 |
13 | if (existingUser.rows.length === 0) {
14 | const newUser = await pool.query(
15 | 'INSERT INTO "userTable" (email, cognito_id) VALUES ($1,$2) RETURNING *',
16 | [email, cognito_id]
17 | );
18 |
19 | console.log("New user added:", newUser.rows[0]);
20 | } else {
21 | console.log("User already exists:", existingUser.rows[0]);
22 | }
23 | next();
24 | } catch (err) {
25 | next({
26 | message: "Error adding user to the database: " + err,
27 | log: err,
28 | });
29 | }
30 | };
31 |
32 | export default addUserToDatabase;
33 |
--------------------------------------------------------------------------------
/server/middleware/auth.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import createCognitoVerifier from "./verifier";
3 | import jwt from "jsonwebtoken";
4 | import { JwtPayload } from "../types";
5 |
6 | const verifier = createCognitoVerifier();
7 |
8 | const auth = async (req: Request, res: Response, next: NextFunction) => {
9 | const token = req.header("Authorization")?.replace("Bearer ", "");
10 | if (!token) {
11 | return res.status(401).send("Unauthorized: No token provided");
12 | }
13 |
14 | try {
15 | let payload: JwtPayload | null = null;
16 | const decoded = jwt.decode(token, { complete: true }) as { payload: JwtPayload } | null;
17 |
18 | if (decoded && decoded.payload && decoded.payload.iss && decoded.payload.iss.includes("cognito")) {
19 | payload = await verifier.verify(token);
20 | } else {
21 | payload = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
22 | }
23 | res.locals.userId = payload!.sub || payload!.user_id;
24 | res.locals.email = payload!.email;
25 | next();
26 | } catch (error) {
27 | return res.status(401).send("Unauthorized: Invalid token");
28 | }
29 | };
30 |
31 | export default auth;
32 |
--------------------------------------------------------------------------------
/server/middleware/awsEncryption.ts:
--------------------------------------------------------------------------------
1 | import crypto from 'crypto';
2 |
3 | const algorithm = 'aes-256-ctr';
4 | const secretKey = process.env.SECRET_KEY!;
5 | const iv = crypto.randomBytes(16);
6 |
7 | const encrypt = (text: string) => {
8 | const cipher = crypto.createCipheriv(algorithm, secretKey, iv);
9 | const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
10 |
11 | return {
12 | iv: iv.toString('hex'),
13 | content: encrypted.toString('hex'),
14 | };
15 | };
16 |
17 | const decrypt = (hash: { iv: string; content: string }) => {
18 | const decipher = crypto.createDecipheriv(
19 | algorithm,
20 | secretKey,
21 | Buffer.from(hash.iv, 'hex')
22 | );
23 |
24 | const decrypted = Buffer.concat([
25 | decipher.update(Buffer.from(hash.content, 'hex')),
26 | decipher.final(),
27 | ]);
28 |
29 | return decrypted.toString();
30 | };
31 |
32 | export { encrypt, decrypt };
33 |
--------------------------------------------------------------------------------
/server/middleware/passportUserMiddleware.ts:
--------------------------------------------------------------------------------
1 | import passport from 'passport';
2 | import jwt from 'jsonwebtoken';
3 | import { Strategy as GoogleStrategy, Profile } from 'passport-google-oauth20';
4 | import { pool } from "../models/db";
5 | import { User } from '../types';
6 | // import db from '../models/db';
7 | passport.use(
8 | new GoogleStrategy(
9 | {
10 | clientID: process.env.GOOGLE_CLIENT_ID as string,
11 | clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
12 | callbackURL: '/api/google/oauth',
13 | },
14 | async (accessToken, refreshToken, profile: Profile, done) => {
15 | const email = profile.emails?.[0].value || '';
16 |
17 | try {
18 |
19 | const userQuery = await pool.query('SELECT * FROM "userTable" WHERE email = $1', [email]);
20 | let user:User;
21 | if (userQuery.rows.length === 0) {
22 |
23 | const newUser = await pool.query(
24 | 'INSERT INTO "userTable" (cognito_id, email) VALUES ($1, $2) RETURNING *',
25 | [email, email]
26 | );
27 | user = newUser.rows[0];
28 | } else {
29 | user = userQuery.rows[0];
30 | }
31 |
32 |
33 | const token = jwt.sign({ user_id: user.cognito_id, email: user.email }, process.env.JWT_SECRET!, { expiresIn: '1h' });
34 | return done(null, { user, token });
35 | } catch (err) {
36 | return done(err, null!);
37 | }
38 | }
39 | )
40 | );
41 | export default passport;
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/server/middleware/verifier.ts:
--------------------------------------------------------------------------------
1 | import { CognitoJwtVerifier } from "aws-jwt-verify";
2 | //verifies the jwt id part of the token
3 | const createCognitoVerifier = () => {
4 | const COGNITO_USER_POOL_ID = process.env.COGNITO_USER_POOL_ID;
5 | const COGNITO_CLIENT_ID = process.env.COGNITO_CLIENT_ID;
6 |
7 | return CognitoJwtVerifier.create({
8 | userPoolId: COGNITO_USER_POOL_ID!,
9 | tokenUse: "id",
10 | clientId: COGNITO_CLIENT_ID!,
11 | });
12 | };
13 |
14 | export default createCognitoVerifier;
--------------------------------------------------------------------------------
/server/middleware/writeToDbAuth.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import { pool } from "../models/db";
3 |
4 | const auth = async (req: Request, res: Response, next: NextFunction) => {
5 | const apiKey = req.headers.authorization?.split(" ")[1];
6 |
7 | if (!apiKey) {
8 | return res.status(401).send("Unauthorized: No API key provided");
9 | }
10 |
11 | try {
12 | //search for a valid api key inside the usertable
13 | const result = await pool.query(
14 | 'SELECT * FROM "userTable" WHERE "api_key" = $1',
15 | [apiKey]
16 | );
17 | if (result.rows.length === 0) {
18 | return res.status(401).send("Unauthorized: Invalid API key");
19 | }
20 |
21 | const user = result.rows[0];
22 | res.locals.user = user.cognito_id;
23 |
24 | next();
25 | } catch (error) {
26 | console.error("Error during API key verification:", error);
27 | return res.status(401).send("Unauthorized: Invalid API key");
28 | }
29 | };
30 |
31 | export default auth;
32 |
--------------------------------------------------------------------------------
/server/models/db.ts:
--------------------------------------------------------------------------------
1 | const { Pool } = require('pg');
2 |
3 | const URI = process.env.DB_URI_STRING
4 |
5 | const pool = new Pool({
6 | connectionString: URI,
7 | });
8 | const checkDatabaseConnection = async () => {
9 | try {
10 | await pool.query('SELECT NOW()');
11 | console.log('Connected to the PostgreSQL database.');
12 | } catch (err) {
13 | console.error('Failed to connect to the PostgreSQL database:', err);
14 | }
15 | };
16 |
17 | export {pool, checkDatabaseConnection};
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "activitytracker.io-backend",
3 | "version": "1.0.0",
4 | "description": "Activitytracker-backend",
5 | "main": "server.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "nodemon server.ts"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "@aws-amplify/auth": "^6.3.14",
14 | "@aws-sdk/client-bedrock": "^3.637.0",
15 | "@aws-sdk/client-bedrock-runtime": "^3.637.0",
16 | "@aws-sdk/client-cognito-identity-provider": "^3.635.0",
17 | "aws-amplify": "^6.5.1",
18 | "aws-jwt-verify": "^4.0.1",
19 | "aws-sdk": "^2.1679.0",
20 | "axios": "^1.7.5",
21 | "bcrypt": "^5.1.1",
22 | "cookie-parser": "^1.4.6",
23 | "cors": "^2.8.5",
24 | "crypto": "^1.0.1",
25 | "dotenv": "^16.4.5",
26 | "express": "^4.19.2",
27 | "google-auth-library": "^9.14.0",
28 | "jsonwebtoken": "^9.0.2",
29 | "nodemon": "^3.1.4",
30 | "passport": "^0.7.0",
31 | "passport-google-oauth20": "^2.0.0",
32 | "passport-jwt": "^4.0.1",
33 | "pg": "^8.12.0",
34 | "puppeteer": "^23.3.0",
35 | "ts-node": "^10.9.2",
36 | "typescript": "^5.5.4"
37 | },
38 | "devDependencies": {
39 | "@types/bcrypt": "^5.0.2",
40 | "@types/cookie-parser": "^1.4.7",
41 | "@types/express": "^4.17.21",
42 | "@types/jsonwebtoken": "^9.0.6",
43 | "@types/pg": "^8.11.6"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/server/routes/aiRoutes.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response, NextFunction } from "express";
2 | const router = express.Router();
3 | import bedrockController from "../controllers/aiController";
4 | import authMiddleware from "../middleware/auth";
5 |
6 |
7 | router.post("/bedrock",authMiddleware, bedrockController.getDataBedrock,(req: Request, res: Response) => {});
8 | export default router;
--------------------------------------------------------------------------------
/server/routes/authRoute.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response, NextFunction } from "express";
2 | import { User } from "../types";
3 | import passport from '../middleware/passportUserMiddleware';
4 | const router = express.Router();
5 |
6 |
7 | router.get('/', passport.authenticate('google', { scope: ['profile', 'email'], session: false }));
8 | // redirects for deployed website
9 | router.get('/oauth',
10 | passport.authenticate('google', { failureRedirect: 'http://os-analytics.com.s3-website-us-west-1.amazonaws.com/login', session: false }),
11 | (req, res) => {
12 | const { token, user } = req.user as User;
13 | res.redirect(`http://os-analytics.com.s3-website-us-west-1.amazonaws.com/login?token=${token}&email=${user.email}`);
14 | }
15 | );
16 |
17 | export default router;
--------------------------------------------------------------------------------
/server/routes/clickRoutes.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response, NextFunction } from "express";
2 | const router = express.Router();
3 | import clickController from "../controllers/clickController";
4 | import authMiddleware from "../middleware/writeToDbAuth";
5 |
6 |
7 | router.post("/",authMiddleware, clickController.storeClickData,(req: Request, res: Response) => {});
8 | router.post("/visits",authMiddleware, clickController.storeVisitData,(req: Request, res: Response) => {});
9 | export default router;
--------------------------------------------------------------------------------
/server/routes/dataRoute.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response, NextFunction } from "express";
2 | const router = express.Router();
3 | import dataController from "../controllers/dataController";
4 | import authMiddleware from "../middleware/auth";
5 |
6 |
7 | router.get("/",authMiddleware, dataController.getAllUserData,(req: Request, res: Response) => {});
8 | router.get("/referral",authMiddleware, dataController.getAllreferralData,(req: Request, res: Response) => {});
9 | router.delete("/delete-website",authMiddleware, dataController.deleteWebsite,(req: Request, res: Response) => {});
10 |
11 |
12 | export default router;
--------------------------------------------------------------------------------
/server/routes/puppeteerRoutes.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response, NextFunction } from "express";
2 | const router = express.Router();
3 | import puppeteerController from "../controllers/puppeteerController";
4 | import authMiddleware from "../middleware/auth";
5 |
6 |
7 | router.get("/",authMiddleware, puppeteerController.takeScreenshot, (req: Request, res: Response, next: NextFunction) => {
8 | res.writeHead(200, {
9 | 'Content-Type': 'image/png',
10 | 'Content-Length': res.locals.screenshotBuffer.length
11 | });
12 | res.end(res.locals.screenshotBuffer);
13 | });
14 |
15 | export default router;
--------------------------------------------------------------------------------
/server/routes/userRoutes.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response, NextFunction } from "express";
2 | const router = express.Router();
3 | import userController from "../controllers/userController";
4 | import addUserToDatabase from "../middleware/addToDB";
5 | import authMiddleware from "../middleware/auth";
6 | router.post(
7 | "/signup",
8 | userController.signup,
9 | addUserToDatabase,
10 | (req: Request, res: Response) => {
11 | res
12 | .status(200)
13 | .send({ email: res.locals.email, userUUID: res.locals.cognito_Id , token :res.locals.token});
14 | }
15 | );
16 |
17 | router.post(
18 | "/login",
19 | userController.login,
20 | (req: Request, res: Response) => {}
21 | );
22 | router.delete(
23 | "/delete-account",
24 | authMiddleware,
25 | userController.deleteAccount,
26 | (req: Request, res: Response) => {}
27 | );
28 |
29 | router.get(
30 | "/activeUser",
31 | authMiddleware,
32 | (req: Request, res: Response) => {
33 | res.status(200).send({ email: res.locals.email, message: "good to go!" });
34 | }
35 | );
36 |
37 | router.get(
38 | "/getApiKey",
39 | authMiddleware,
40 | userController.getApiKey,
41 | (req: Request, res: Response) => {;
42 | }
43 | );
44 |
45 |
46 | router.delete('/apiKey',authMiddleware, userController.deleteApiKey);
47 | router.put('/apiKey', authMiddleware,userController.refreshApiKey);
48 | router.put('/awsCredentials', authMiddleware,userController.addAwsCredentials);
49 |
50 | router.post(
51 | "/logout",
52 | userController.logout,
53 | (req: Request, res: Response) => {}
54 | );
55 |
56 |
57 | router.post('/forgot-password', userController.forgotPassword);
58 | router.post('/confirm-password', userController.confirmPassword);
59 | export default router;
60 |
--------------------------------------------------------------------------------
/server/server.ts:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | import express, { Request, Response, NextFunction } from 'express';
3 | import cookieParser from 'cookie-parser';
4 | import {checkDatabaseConnection } from './models/db';
5 | import userRoutes from './routes/userRoutes';
6 | import clickRoutes from './routes/clickRoutes';
7 | import dataRoutes from './routes/dataRoute';
8 | import aiRoutes from './routes/aiRoutes';
9 | import authRoutes from './routes/authRoute'
10 | import passport from './middleware/passportUserMiddleware';
11 | import puppeteerRoutes from './routes/puppeteerRoutes';
12 |
13 |
14 |
15 | const cors = require('cors');
16 |
17 |
18 | //check db connection
19 | checkDatabaseConnection();
20 |
21 | const app = express();
22 | app.use(express.json());
23 | app.use(cors());
24 | app.use(cookieParser());
25 | app.use(passport.initialize());
26 | const port = 8080;
27 | // app.get('/api',authMiddleware, (req: Request, res: Response) => {
28 | // res.json({ message: 'Hello from server!',
29 | // user: res.locals.userId,
30 | // });
31 | // });
32 | app.use('/api/google',authRoutes)
33 | app.use('/api/auth',userRoutes)
34 | app.use('/api/click-data',clickRoutes)
35 | app.use('/api/data',dataRoutes)
36 |
37 | app.use('/api/ai',aiRoutes)
38 |
39 | app.use('/api/screenshot', puppeteerRoutes)
40 |
41 |
42 | // app.use('/api/oauth',oauthRoute)
43 | // app.use('/api/oauthrequest',oauthRequestRoute)
44 |
45 | //Error handling
46 |
47 | app.use((req: Request, res: Response) => {
48 | res.status(404).send("This is not the page you're looking for...");
49 | });
50 |
51 |
52 | app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
53 | const defaultErr = {
54 | log: 'Express error handler caught unknown middleware error',
55 | status: 500,
56 | message: { err: 'An error occurred' },
57 | };
58 |
59 | const errorObj = Object.assign({}, defaultErr, err);
60 |
61 | console.error(errorObj.log);
62 | return res.status(errorObj.status).json(errorObj.message);
63 | });
64 | app.listen(port, () => {
65 | console.log(`Server running on http://localhost:${port}`);
66 | });
67 |
--------------------------------------------------------------------------------
/server/types.ts:
--------------------------------------------------------------------------------
1 | export type ClickData = {
2 | page_url?: string;
3 | website_name?: string;
4 | total_clicks: number;
5 | avg_x_coord?: number;
6 | avg_y_coord?: number;
7 | };
8 |
9 | type OpenAIChoice = {
10 | message: {
11 | role: string;
12 | content: string;
13 | };
14 | };
15 |
16 | export type OpenAIResponse = {
17 | choices: OpenAIChoice[];
18 | };
19 |
20 | export type JwtPayload = {
21 | sub?: string;
22 | user_id?: string;
23 | email?: string;
24 | iss?: string;
25 | };
26 |
27 | export type User = {
28 | cognito_id: string;
29 | email: string;
30 | token: string;
31 | user: {
32 | email: string;
33 | };
34 | };
35 |
--------------------------------------------------------------------------------