├── .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 |
13 |
14 |
15 |
16 |
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 | Screenshot 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 | 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 | 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 | {/* Map Logo */} 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | {/* Map Logo */} 37 | 38 |
39 | 61 |
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 | {/*
Icons made by Prosymbols from www.flaticon.com'
*/} -------------------------------------------------------------------------------- /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 | Dashboard Preview 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 | 56 | 57 | 58 | 59 | 62 | 63 | 64 |
65 | 66 | 72 | product demo 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 | {item.title} 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 |
67 | 68 | Meet the Team 69 | 70 | 71 | {teamMembers.map((member, index) => ( 72 | 73 | 78 | 91 | 96 | 97 | {member.name} 98 | 99 | 106 | 107 | 108 | 115 | 116 | 117 | 118 | 119 | 120 | ))} 121 | 122 |
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 |
6 |
7 |
8 |
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 |
96 | 108 | 121 | 134 | 141 | Create account 142 | 143 | 144 | 145 | 151 | 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 | at-logo 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 | 68 | 69 | { 72 | onLogoutClick(); 73 | }} 74 | > 75 | 78 | 79 | 80 | ) : ( 81 | <> 82 | 83 | 84 | 85 | 86 | GitHub 87 | 88 | 91 | 92 | 93 | 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 | 71 | 72 | 73 | ) : ( 74 | <> 75 | 76 | 81 | 82 | 83 | 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 | 101 | 102 | 103 | 104 | 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 | 25 | 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 | 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 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | {displayedItems} 47 | 48 |
DateTimeInteractionElementBrowserOSWebsite
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 | 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 | 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 | AT.io 18 |

OS Analytics

19 |
20 | 21 |
22 |
    23 |
  • 24 | setSelectedWebsite('overview')} 28 | > 29 | overview 30 | {"Overview"} 31 | 32 |
  • 33 | {websites.map((website, index) => ( 34 |
  • 35 | setSelectedWebsite(website)} 39 | > 40 | {website} 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 | --------------------------------------------------------------------------------