├── .gitignore ├── .vercelignore ├── README.md ├── __tests__ ├── Dashboard.tests.js ├── Homepage.tests.js ├── Login.tests.js ├── Onboarding.tests.js └── SignUp.tests.js ├── data.json ├── dataBuild.json ├── dataBundle.json ├── jest.config.js ├── jsconfig.json ├── next.config.mjs ├── nextlevelpackage ├── .gitignore ├── README.md ├── cli.js ├── index.js ├── node_modules │ └── .package-lock.json ├── package-lock.json ├── package.json └── sendToApi.js ├── package-lock.json ├── package.json ├── public ├── HEADSHOTS │ ├── fred.png │ ├── fredBW.png │ ├── ian.png │ ├── ianBW.png │ ├── kim.png │ ├── kimBW.png │ ├── nelly.png │ └── nellyBW.png ├── Icon.png ├── LOADINGLOGO.mp4 ├── METRICS │ ├── buildtime.svg │ ├── bundlesize.svg │ ├── cls.svg │ ├── fcp.svg │ ├── fid.svg │ ├── inp.svg │ ├── lcp.svg │ └── ttfb.svg ├── NEXTLEVEL.mp4 ├── NEXTLEVELANI.mp4 ├── NextLevelBanner.png ├── NextLevelDashboard.gif ├── NextLevelDashboardHomepage.gif ├── NextLevelGifHomepage.gif ├── TopNavLogo.png ├── TransparentGifLogo.gif ├── TransparentIcon.png ├── TransparentLogo.png ├── TransparentLogoLessSpace.png ├── UpdatedNextLevelGifHomepage.gif ├── github.png ├── laptop.png ├── linkedin.png ├── next.svg └── transparent-gif-v2-_1_.svg ├── src ├── app │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ ├── callback │ │ │ │ └── [provider].js │ │ │ │ └── route.js │ │ ├── login │ │ │ └── route.js │ │ └── signup │ │ │ └── route.js │ ├── components │ │ ├── Modal.css │ │ ├── Modal.js │ │ ├── SessionWrapper.tsx │ │ ├── Spinner.js │ │ ├── Spinner.module.css │ │ ├── topnav.js │ │ ├── topnav.module.css │ │ └── withAuth.js │ ├── dashboard │ │ ├── api │ │ │ ├── build │ │ │ │ └── route.js │ │ │ ├── bundle │ │ │ │ └── route.js │ │ │ ├── middleware.js │ │ │ └── webvitals │ │ │ │ └── route.js │ │ ├── components │ │ │ ├── APIkey.js │ │ │ ├── BuildTimeChart.js │ │ │ ├── BuildTimeContainer.js │ │ │ ├── BundleLog.js │ │ │ ├── CLSChart.js │ │ │ ├── Rating.js │ │ │ ├── Sidebar.js │ │ │ ├── WebVitalRatings.js │ │ │ ├── WebVitalsChart.js │ │ │ ├── WebVitalsContainer.js │ │ │ └── WebVitalsFilter.js │ │ ├── dashboard.module.css │ │ ├── hooks │ │ │ ├── useBuildTimeData.js │ │ │ ├── useBundleData.js │ │ │ └── useWebVitalsData.js │ │ ├── layout.js │ │ ├── page.js │ │ └── settings │ │ │ └── page.js │ ├── favicon.ico │ ├── globals.css │ ├── home.css │ ├── layout.js │ ├── lib │ │ ├── auth.js │ │ └── connectDB.js │ ├── login │ │ ├── 4k-tech-untb6o7k25k9gvy1.jpg │ │ ├── TransparentIcon.png │ │ ├── login.css │ │ ├── page.js │ │ └── transparent gif v2 (1).gif │ ├── models │ │ └── User.js │ ├── onboarding │ │ ├── Onboarding.module.css │ │ ├── api │ │ │ └── route.js │ │ ├── components │ │ │ ├── CodeBox.js │ │ │ ├── CodeBox.module.css │ │ │ ├── CopyButton.js │ │ │ ├── CopyButton.module.css │ │ │ ├── NextButton.js │ │ │ ├── NextButton.module.css │ │ │ └── Step.js │ │ └── page.js │ ├── page.js │ └── signup │ │ ├── page.js │ │ └── signUp.css └── instrumentation.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | #env 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /.vercelignore: -------------------------------------------------------------------------------- 1 | nextlevelpackage/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | 5 |

6 | 7 |
8 | 9 |
10 | 11 | ![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) 12 | ![NextJS](https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white) 13 | ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) 14 | ![ChartJS](https://img.shields.io/badge/Chart%20js-FF6384?style=for-the-badge&logo=chartdotjs&logoColor=white) 15 | ![Node](https://img.shields.io/badge/-node-339933?style=for-the-badge&logo=node.js&logoColor=white) 16 | ![MongoDB](https://img.shields.io/badge/MongoDB-4EA94B?style=for-the-badge&logo=mongodb&logoColor=white) 17 | ![Vercel](https://img.shields.io/badge/Vercel-000000?style=for-the-badge&logo=vercel&logoColor=white) 18 | ![GoogleCloud](https://img.shields.io/badge/Google_Cloud-4285F4?style=for-the-badge&logo=google-cloud&logoColor=white) 19 | ![GitHubActions](https://img.shields.io/badge/GitHub_Actions-2088FF?style=for-the-badge&logo=github-actions&logoColor=white) 20 | ![Jest](https://img.shields.io/badge/-jest-C21325?style=for-the-badge&logo=jest&logoColor=white) 21 | ![Canva](https://img.shields.io/badge/Canva-%2300C4CC.svg?&style=for-the-badge&logo=Canva&logoColor=white) 22 | ![Jira](https://img.shields.io/badge/Jira-0052CC?style=for-the-badge&logo=Jira&logoColor=white) 23 | ![Zoom](https://img.shields.io/badge/Zoom-2D8CFF?style=for-the-badge&logo=zoom&logoColor=white) 24 | ![VSCode](https://img.shields.io/badge/VSCode-0078D4?style=for-the-badge&logo=visual%20studio%20code&logoColor=white) 25 | ![JSON](https://img.shields.io/badge/json-5E5C5C?style=for-the-badge&logo=json&logoColor=white) 26 | ![Prettier](https://img.shields.io/badge/prettier-1A2C34?style=for-the-badge&logo=prettier&logoColor=F7BA3E) 27 | ![MacOS](https://img.shields.io/badge/mac%20os-000000?style=for-the-badge&logo=apple&logoColor=white) 28 | ![Windows11](https://img.shields.io/badge/Windows_11-0078d4?style=for-the-badge&logo=windows-11&logoColor=white) 29 | ![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white) 30 | ![StackOverflow](https://img.shields.io/badge/Stack_Overflow-FE7A16?style=for-the-badge&logo=stack-overflow&logoColor=white) 31 | ![Powershell](https://img.shields.io/badge/powershell-5391FE?style=for-the-badge&logo=powershell&logoColor=white) 32 | ![Fiverr](https://img.shields.io/badge/fiverr-1DBF73?style=for-the-badge&logo=fiverr&logoColor=white) 33 | 34 |
35 | 36 | # 37 | 38 | ![Website](https://img.shields.io/badge/Website-B9D9EB) 39 | ![LinkedIn](https://img.shields.io/badge/LinkedIn-B9D9EB) 40 | ![npm](https://img.shields.io/badge/npm-B9D9EB) 41 | 42 | In recent years, Next.js has gained immense popularity with developers, however many struggle to optimize the performance of their Next.js applications due to a lack of visibility into specific build and runtime metrics, making it difficult to identify and resolve bottlenecks efficiently. 43 | 44 | NextLevel is a performance metrics dashboard tailored to Next.js applications that visualizes critical data, such as build time and key web vitals, enabling developers to pinpoint inefficiencies and improve development productivity and end-user experience. 45 | 46 |

47 | 48 |

49 | 50 | ## Getting Started 51 | 52 | 1. To get started, visit nextlevel-dash.com and create an account. After account creation, you will be navigated to an onboarding page which will direct you to complete the below steps. 53 | 54 | 2. Install configure next.js bundle analyzer in your Next.js application 55 | 56 | NPM install Next.js Bundle Analyzer: 57 | ```bash 58 | npm install @next/bundle-analyzer 59 | ``` 60 | 61 | Configure next.config.mjs file: 62 | ```bash 63 | import pkg from '@next/bundle-analyzer'; 64 | const withBundleAnalyzer = pkg({ 65 | enabled: process.env.ANALYZE === 'true', 66 | }); 67 | 68 | const nextConfig = {}; 69 | 70 | export default withBundleAnalyzer(nextConfig); 71 | ``` 72 | 73 | 3. Install and configure our npm package, NextLevelPackage, through the terminal. It can also be found [here](https://www.npmjs.com/package/nextlevelpackage). 74 | 75 | NPM Install NextLevelPackage: 76 | ```bash 77 | npm install nextlevelpackage 78 | ``` 79 | 80 | Import NextLevelPackage in layout.js: 81 | ```bash 82 | import NextWebVitals from 'nextlevelpackage'; 83 | ``` 84 | 85 | Add NextWebVitals component in RootLayout body: 86 | ```bash 87 | export default function RootLayout({ children }) { 88 | return ( 89 | 90 | 91 | 92 | {children} 93 | 94 | 95 | ); 96 | } 97 | ``` 98 | 99 | 4. Configure yoour Environmental Variables 100 | 101 | Add the following line to your .env.local file: 102 | ```bash 103 | NEXT_PUBLIC_API_KEY= 104 | ``` 105 | When you create an account, the setup page will provide you with your API key. 106 | 107 | 5. Add Build Script to package.json 108 | 109 | Add the following script to your package.json: 110 | ```bash 111 | "scripts": { 112 | "nextlevelbuild": "node ./node_modules/nextlevelpackage/cli.js" 113 | } 114 | ``` 115 | 116 | Run this build script instead of 'npm next build' to track metrics in the dashboard: 117 | ```bash 118 | npm run nextlevelbuild 119 | ``` 120 | 121 | 6. Navitage to your NextLevel dashboard to view tracked metrics! 122 | 123 | ## Tracked Metrics 124 | 125 | Metrics displayed on the dashboard include: 126 | 127 | - Time to First Byte: 128 | - measures the time taken from a user's request to the first byte of data received from the server, indicating server responsiveness. 129 | - Largest Contentful Paint: 130 | - gauges the time it takes for the largest visible content element on a webpage to load, impacting user perception of loading speed. 131 | - First Contentful Paint: 132 | - tracks the time from page load to when the first piece of content is rendered on the screen, marking the start of the visual loading process. 133 | - First Input Delay: 134 | - measures the delay between the user's first interaction with the page and the browser's response, reflecting input responsiveness. 135 | - Interaction to Next Paint: 136 | - evaluates the time from user interaction to the next visual update, assessing how quickly a page responds to user inputs. 137 | - Cumulative Layout Shift: 138 | - quantifies the total of all individual layout shifts that occur during the entire lifespan of the page, indicating visual stability. 139 | - Build Time: 140 | - refers to the duration taken to compile and bundle code and assets into a deployable format during the development process. 141 | - Bundle Size: 142 | - denotes the total size of all the compiled assets sent to the client, affecting load times and overall performance. 143 | 144 | By default, the overview data covers the last 24 hours. You can modify the time period using the date and time selector located in the top right corner of the dashboard. 145 | 146 | There are also gauges that track the averages of each metric over a set period of time. From these averages there will also be a performance score displayed ranging from "poor" to "needs work" to "great." This will notify you if your application needs to make some changes to improve its performance. 147 | 148 | ## Contribution Guidelines 149 | 150 | ### Contribution Method 151 | 152 | We welcome your contributions to the NextLevel product! 153 | 154 | 1. Fork the repo 155 | 2. Create your feature branch (`git checkout -b feature/newFeature`) and create your new feature 156 | 3. Commit your changes (`git commit -m 'Added [new-feature-description]'`) 157 | 4. Push to the branch (`git push origin feature/newFeature`) 158 | 5. Make a Pull Request 159 | 6. The NextLevel team will review the feature and approve! 160 | 161 | ### Looking Ahead 162 | 163 | Here is a list of features being considered by our team: 164 | 165 | - Tracking individual page/component build time via the npm package 166 | - Allow users to track metrics for multiple codebases through their dashboard 167 | - Create organizations so multiple users can access an organization's dashboard 168 | - Enhanced security through change password and email functionality 169 | - A dark mode and light mode feature to reduce eye strain 170 | - A completed forgot password and remember me functionality 171 | - Fix CLS gauge so that the marker is at the correct position 172 | 173 | ## Contributors 174 | 175 | - Frederico Aires da Neto: [GitHub](https://github.com/FredAires) | [LinkedIn](https://www.linkedin.com/in/frederico-neto-a3722b221/) 176 | - Kim Cuomo: [GitHub](https://github.com/kimcuomo) | [LinkedIn](https://www.linkedin.com/in/kimcuomo/) 177 | - Ian Mann: [GitHub](https://github.com/ianmannn) | [LinkedIn](https://www.linkedin.com/in/iancmann99/) 178 | - Nelly Segimoto: [GitHub](https://github.com/nellysegi) | [LinkedIn](https://www.linkedin.com/in/nellysegimoto/) 179 | 180 | ## License 181 | 182 | Distributed under the MIT License. See LICENSE for more information. 183 | -------------------------------------------------------------------------------- /__tests__/Dashboard.tests.js: -------------------------------------------------------------------------------- 1 | // __tests__/Dashboard.test.js 2 | import React from 'react'; 3 | import '@testing-library/jest-dom' 4 | import { render, screen } from '@testing-library/react'; 5 | import Dashboard from '../src/app/dashboard/page'; 6 | import withAuth from '../src/app/components/withAuth'; 7 | 8 | jest.mock('../src/app/dashboard/components/sidebar', () => () =>
Mocked SideBar
); 9 | jest.mock('../src/app/dashboard/components/APIkey', () => () =>
Mocked APIKey
); 10 | jest.mock('../src/app/dashboard/components/WebVitalsContainer', () => () =>
Mocked WebVitalsContainer
); 11 | jest.mock('../src/app/dashboard/components/buildtimecontainer', () => () =>
Mocked BuildTimeContainer
); 12 | 13 | // Mocking withAuth higher-order component 14 | jest.mock('../src/app/components/withAuth', () => (Component) => (props) => ); 15 | 16 | describe('Dashboard Component', () => { 17 | const props = { 18 | searchParams: { 19 | username: 'testuser', 20 | }, 21 | }; 22 | 23 | it('renders without crashing', () => { 24 | render(); 25 | expect(screen.getByText('Mocked SideBar')).toBeInTheDocument(); 26 | expect(screen.getByText('Mocked APIKey')).toBeInTheDocument(); 27 | expect(screen.getByText('Mocked WebVitalsContainer')).toBeInTheDocument(); 28 | expect(screen.getByText('Mocked BuildTimeContainer')).toBeInTheDocument(); 29 | }); 30 | 31 | it('passes the correct username to the SideBar component', () => { 32 | render(); 33 | const sideBarElement = screen.getByText('Mocked SideBar'); 34 | expect(sideBarElement).toBeInTheDocument(); 35 | }); 36 | 37 | it('passes the correct username to the APIKey component', () => { 38 | render(); 39 | const apiKeyElement = screen.getByText('Mocked APIKey'); 40 | expect(apiKeyElement).toBeInTheDocument(); 41 | }); 42 | 43 | it('passes the correct username to the WebVitalsContainer component', () => { 44 | render(); 45 | const webVitalsElement = screen.getByText('Mocked WebVitalsContainer'); 46 | expect(webVitalsElement).toBeInTheDocument(); 47 | }); 48 | 49 | it('passes the correct username to the BuildTimeContainer component', () => { 50 | render(); 51 | const buildTimeElement = screen.getByText('Mocked BuildTimeContainer'); 52 | expect(buildTimeElement).toBeInTheDocument(); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /__tests__/Homepage.tests.js: -------------------------------------------------------------------------------- 1 | // __tests__/Home.test.js 2 | import React from 'react'; 3 | import { render, screen, act } from '@testing-library/react'; 4 | import Home from '../src/app/page'; 5 | import '@testing-library/jest-dom'; 6 | 7 | // Mock next/link 8 | jest.mock('next/link', () => { 9 | return ({ children }) => { 10 | return children; 11 | }; 12 | }); 13 | 14 | // Mock next/image 15 | jest.mock('next/image', () => { 16 | return (props) => { 17 | return ; 18 | }; 19 | }); 20 | 21 | describe('Home Component', () => { 22 | beforeEach(() => { 23 | // Mock the scroll event 24 | global.scrollY = 0; 25 | window.addEventListener = jest.fn(); 26 | window.removeEventListener = jest.fn(); 27 | }); 28 | 29 | it('renders without crashing', () => { 30 | render(); 31 | expect(screen.getByText('Take your application to the')).toBeInTheDocument(); 32 | expect(screen.getByText('STEP ONE')).toBeInTheDocument(); 33 | expect(screen.getByText('STEP TWO')).toBeInTheDocument(); 34 | expect(screen.getByText('STEP THREE')).toBeInTheDocument(); 35 | expect(screen.getByText('Tracked Metrics Include :')).toBeInTheDocument(); 36 | }); 37 | 38 | it('adds "show-animate" class on mount', () => { 39 | const { container } = render(); 40 | const section = container.querySelector('.sec-1'); 41 | expect(section).toHaveClass('show-animate'); 42 | }); 43 | 44 | it('adds "show-animate" class on scroll', () => { 45 | const { container } = render(); 46 | const sections = container.querySelectorAll('section'); 47 | 48 | act(() => { 49 | window.scrollY = 400; 50 | window.dispatchEvent(new Event('scroll')); 51 | }); 52 | 53 | sections.forEach((section) => { 54 | expect(section).toHaveClass('show-animate'); 55 | }); 56 | }); 57 | 58 | it('removes event listener on unmount', () => { 59 | const { unmount } = render(); 60 | unmount(); 61 | expect(window.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function)); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /__tests__/Login.tests.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { render, screen, fireEvent, waitFor } from '@testing-library/react'; 3 | import { signIn, useSession } from 'next-auth/react'; 4 | import Login from '../src/app/login/page.js'; 5 | import Spinner from '../src/app/components/Spinner.js'; 6 | import React from 'react'; 7 | 8 | // Mock the necessary components and functions 9 | jest.mock('next-auth/react'); 10 | jest.mock('../src/app/components/Spinner.js', () => () =>
Loading...
); 11 | jest.mock('next/image', () => (props) => ); 12 | 13 | describe('Login', () => { 14 | const mockSignIn = jest.fn(); 15 | const mockUseSession = useSession; 16 | 17 | beforeEach(() => { 18 | mockUseSession.mockReturnValue({ 19 | data: null, 20 | status: 'unauthenticated' 21 | }); 22 | signIn.mockImplementation(mockSignIn); 23 | }); 24 | 25 | afterEach(() => { 26 | jest.clearAllMocks(); 27 | }); 28 | 29 | it('renders the login form', async () => { 30 | render(); 31 | 32 | await waitFor(() => expect(screen.getByPlaceholderText('Username')).toBeInTheDocument()); 33 | 34 | expect(screen.getByPlaceholderText('Username')).toBeInTheDocument(); 35 | expect(screen.getByPlaceholderText('Password')).toBeInTheDocument(); 36 | expect(screen.getByText('Login')).toBeInTheDocument(); 37 | expect(screen.getByText("Don't have an account?")).toBeInTheDocument(); 38 | }); 39 | 40 | it('displays error message on login failure', async () => { 41 | mockSignIn.mockResolvedValueOnce({ error: 'Invalid credentials' }); 42 | render(); 43 | 44 | await waitFor(() => expect(screen.getByPlaceholderText('Username')).toBeInTheDocument()); 45 | 46 | fireEvent.change(screen.getByPlaceholderText('Username'), { target: { value: 'testuser' } }); 47 | fireEvent.change(screen.getByPlaceholderText('Password'), { target: { value: 'password' } }); 48 | fireEvent.click(screen.getByText('Login')); 49 | 50 | await waitFor(() => expect(screen.getByText('Invalid credentials')).toBeInTheDocument()); 51 | }); 52 | 53 | it('redirects to dashboard on successful login', async () => { 54 | // Mock window.location.href 55 | delete window.location; 56 | window.location = { href: '' }; 57 | 58 | mockSignIn.mockResolvedValueOnce({ error: null }); 59 | render(); 60 | 61 | await waitFor(() => expect(screen.getByPlaceholderText('Username')).toBeInTheDocument()); 62 | 63 | fireEvent.change(screen.getByPlaceholderText('Username'), { target: { value: 'testuser' } }); 64 | fireEvent.change(screen.getByPlaceholderText('Password'), { target: { value: 'password' } }); 65 | fireEvent.click(screen.getByText('Login')); 66 | 67 | await waitFor(() => expect(screen.getByText('Loading...')).toBeInTheDocument()); 68 | 69 | await waitFor(() => expect(window.location.href).toBe('/dashboard/?username=testuser')); 70 | }); 71 | 72 | it('renders the spinner when loading', () => { 73 | render(); 74 | expect(screen.getByText('Loading...')).toBeInTheDocument(); 75 | }); 76 | 77 | it('handles OAuth sign-in error', async () => { 78 | mockSignIn.mockResolvedValueOnce({ error: 'OAuth error' }); 79 | render(); 80 | 81 | await waitFor(() => expect(screen.getByPlaceholderText('Username')).toBeInTheDocument()); 82 | 83 | // Changed the selector to use aria-label 84 | fireEvent.click(screen.getByRole('button', { name: 'Sign in with Google' })); 85 | 86 | await waitFor(() => expect(screen.getByText('Failed to sign in with google. Please try again.')).toBeInTheDocument()); 87 | }); 88 | 89 | it('redirects to dashboard on successful OAuth sign-in', async () => { 90 | // Mock window.location.href 91 | delete window.location; 92 | window.location = { href: '' }; 93 | 94 | mockUseSession.mockReturnValue({ 95 | data: { user: { email: 'testuser' } }, 96 | status: 'authenticated', 97 | }); 98 | 99 | render(); 100 | 101 | await waitFor(() => expect(screen.getByPlaceholderText('Username')).toBeInTheDocument()); 102 | 103 | // Changed the selector to use aria-label 104 | fireEvent.click(screen.getByRole('button', { name: 'Sign in with Google' })); 105 | 106 | await waitFor(() => expect(screen.getByText('Loading...')).toBeInTheDocument()); 107 | 108 | await waitFor(() => expect(window.location.href).toBe('/dashboard/?username=testuser')); 109 | }); 110 | 111 | it('applies correct styles on mount and cleans up on unmount', () => { 112 | const { unmount } = render(); 113 | const bodyStyles = document.body.style; 114 | 115 | expect(bodyStyles.fontFamily).toBe("'Poppins', sans-serif"); 116 | expect(bodyStyles.display).toBe('flex'); 117 | expect(bodyStyles.justifyContent).toBe('center'); 118 | expect(bodyStyles.alignItems).toBe('center'); 119 | expect(bodyStyles.minHeight).toBe('100vh'); 120 | expect(bodyStyles.backgroundImage).toContain('https://getwallpapers.com/wallpaper/full/2/8/f/537844.jpg'); 121 | expect(bodyStyles.backgroundRepeat).toBe('no-repeat'); 122 | expect(bodyStyles.backgroundSize).toBe('cover'); 123 | expect(bodyStyles.backgroundPosition).toBe('center'); 124 | expect(bodyStyles.color).toBe('rgb(255, 255, 255)'); 125 | 126 | unmount(); 127 | 128 | expect(bodyStyles.fontFamily).toBe(''); 129 | expect(bodyStyles.display).toBe(''); 130 | expect(bodyStyles.justifyContent).toBe(''); 131 | expect(bodyStyles.alignItems).toBe(''); 132 | expect(bodyStyles.minHeight).toBe(''); 133 | expect(bodyStyles.backgroundImage).toBe(''); 134 | expect(bodyStyles.backgroundRepeat).toBe('no-repeat'); // Changed to check if 'no-repeat' is retained 135 | expect(bodyStyles.backgroundSize).toBe('auto'); 136 | expect(bodyStyles.backgroundPosition).toBe('0% 0%'); 137 | expect(bodyStyles.color).toBe(''); 138 | }); 139 | }); 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /__tests__/Onboarding.tests.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, waitFor, fireEvent } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import Onboarding from '../src/app/onboarding/page'; 5 | import Step from '../src/app/onboarding/components/Step'; 6 | import NextButton from '../src/app/onboarding/components/NextButton'; 7 | import CopyButton from '../src/app/onboarding/components/CopyButton'; 8 | import CodeBox from '../src/app/onboarding/components/CodeBox'; 9 | 10 | jest.mock('../src/app/onboarding/components/Step', () => jest.fn(() =>
Step Component
)); 11 | jest.mock('../src/app/onboarding/components/NextButton', () => jest.fn(() =>
Next Button
)); 12 | jest.mock('../src/app/components/withAuth', () => (Component) => (props) => ); 13 | jest.mock('../src/app/onboarding/components/CodeBox', () => jest.fn(() =>
CodeBox Component
)); 14 | 15 | describe('Onboarding Page', () => { 16 | const mockOnboardingSteps = [ 17 | { 18 | stepNumber: 1, 19 | title: "Install and Configure Next.js Bundle Analyzer", 20 | description: "NPM install Next.js Bundle Analyzer:", 21 | code: `npm install @next/bundle-analyzer`, 22 | language: "terminal", 23 | api: false, 24 | }, 25 | { 26 | stepNumber: "", 27 | title: "", 28 | description: "Configure next.config.mjs file:", 29 | code: `import pkg from '@next/bundle-analyzer'; 30 | const withBundleAnalyzer = pkg({ 31 | enabled: process.env.ANALYZE === 'true', 32 | }); 33 | 34 | const nextConfig = {}; 35 | 36 | export default withBundleAnalyzer(nextConfig);`, 37 | language: "next.config.mjs", 38 | api: false, 39 | }, 40 | { 41 | stepNumber: 2, 42 | title: "Install and configure NextLevelPackage", 43 | description: "NPM Install NextLevelPackage:", 44 | code: `npm install nextlevelpackage`, 45 | language: "terminal", 46 | api: false, 47 | }, 48 | { 49 | stepNumber: "", 50 | title: "", 51 | description: "Import NextLevelPackage in layout.js:", 52 | code: `import NextWebVitals from 'nextlevelpackage';`, 53 | language: "layout.js", 54 | api: false, 55 | }, 56 | { 57 | stepNumber: "", 58 | title: "", 59 | description: "Add NextWebVitals component in RootLayout body:", 60 | code: `export default function RootLayout({ children }) { 61 | return ( 62 | 63 | 64 | 65 | {children} 66 | 67 | 68 | ); 69 | }`, 70 | language: "layout.js", 71 | api: false, 72 | }, 73 | { 74 | stepNumber: 3, 75 | title: 'Configure Environment Variables', 76 | description: 'Add the following line to your .env.local file:', 77 | code: `NEXT_PUBLIC_API_KEY=`, 78 | language: '.env.local', 79 | api: true, 80 | }, 81 | { 82 | stepNumber: 4, 83 | title: "Add Build Script to package.json", 84 | description: "Add the following script to your package.json:", 85 | code: `"scripts": { 86 | "nextlevelbuild": "node ./node_modules/nextlevelpackage/cli.js" 87 | }`, 88 | language: "terminal", 89 | api: false, 90 | }, 91 | { 92 | stepNumber: '', 93 | title: '', 94 | description: 'Run this build script instead of \'npm next build\' to track metrics in the dashboard:', 95 | code: `npm run nextlevelbuild`, 96 | language: "terminal", 97 | api: false, 98 | }, 99 | ]; 100 | const mockProps = { 101 | searchParams: { 102 | username: 'testuser', 103 | }, 104 | }; 105 | beforeEach(() => { 106 | Step.mockClear(); 107 | NextButton.mockClear(); 108 | }); 109 | 110 | test('renders onboarding page with steps', () => { 111 | render(); 112 | 113 | expect(screen.getByText('NextLevel Onboarding Instructions')).toBeInTheDocument(); 114 | expect(Step).toHaveBeenCalledTimes(mockOnboardingSteps.length); 115 | }); 116 | 117 | test('renders Step components with correct props', () => { 118 | render(); 119 | 120 | mockOnboardingSteps.forEach((step, index) => { 121 | expect(Step).toHaveBeenNthCalledWith( 122 | index + 1, 123 | expect.objectContaining({ 124 | className: "step", 125 | stepNumber: step.stepNumber, 126 | title: step.title, 127 | description: step.description, 128 | code: step.code, 129 | language: step.language, 130 | api: step.api, 131 | username: 'testuser', 132 | }), 133 | {} 134 | ); 135 | }); 136 | }); 137 | 138 | test('renders NextButton with correct props', () => { 139 | render(); 140 | expect(screen.getByText('Next Button')).toBeInTheDocument(); 141 | expect(NextButton).toHaveBeenCalledWith(expect.objectContaining({ username: 'testuser' }), {}); 142 | }); 143 | jest.unmock('../src/app/onboarding/components/Step'); 144 | }); 145 | 146 | describe('Step Component', () => { 147 | const mockStepProps = { 148 | stepNumber: 1, 149 | title: 'Test Step', 150 | description: 'Test Description', 151 | code: 'test code', 152 | language: 'test language', 153 | api: false, 154 | username: 'testuser', 155 | }; 156 | test('renders Step component', () => { 157 | render(); 158 | expect(screen.getByText('Step Component')).toBeInTheDocument(); 159 | }); 160 | }); 161 | 162 | describe('CopyButton Component', () => { 163 | test('renders CopyButton component', () => { 164 | render(); 165 | expect(screen.getByText('Copy')).toBeInTheDocument(); 166 | }); 167 | 168 | test('copies text to clipboard on click', async () => { 169 | Object.assign(navigator, { 170 | clipboard: { 171 | writeText: jest.fn().mockResolvedValue(), 172 | }, 173 | }); 174 | 175 | render(); 176 | fireEvent.click(screen.getByText('Copy')); 177 | 178 | expect(navigator.clipboard.writeText).toHaveBeenCalledWith('test text'); 179 | expect(await screen.findByText('Copied!')).toBeInTheDocument(); 180 | }); 181 | }); -------------------------------------------------------------------------------- /__tests__/SignUp.tests.js: -------------------------------------------------------------------------------- 1 | // __tests__/Signup.test.js 2 | import React from 'react'; 3 | import { render, screen, fireEvent, waitFor } from '@testing-library/react'; 4 | import Signup from '../src/app/signup/page.js'; 5 | import { useSession, signIn } from 'next-auth/react'; 6 | import '@testing-library/jest-dom' 7 | 8 | jest.mock('next-auth/react'); 9 | jest.mock('next/link', () => ({ children }) => children); 10 | 11 | describe('Signup Component', () => { 12 | beforeEach(() => { 13 | useSession.mockReturnValue({ data: null, status: 'unauthenticated' }); 14 | }); 15 | 16 | it('renders without crashing', () => { 17 | render(); 18 | expect(screen.getAllByText('Sign Up').length).toBeGreaterThan(0); 19 | expect(screen.getByPlaceholderText('Email')).toBeInTheDocument(); 20 | expect(screen.getByPlaceholderText('Password')).toBeInTheDocument(); 21 | expect(screen.getByPlaceholderText('Confirm Password')).toBeInTheDocument(); 22 | }); 23 | 24 | it('displays error if passwords do not match', async () => { 25 | render(); 26 | fireEvent.change(screen.getByPlaceholderText('Email'), { target: { value: 'testuser' } }); 27 | fireEvent.change(screen.getByPlaceholderText('Password'), { target: { value: 'password123' } }); 28 | fireEvent.change(screen.getByPlaceholderText('Confirm Password'), { target: { value: 'password124' } }); 29 | fireEvent.click(screen.getByRole('button', { name: /Sign Up/i })); 30 | 31 | expect(await screen.findByText('Passwords do not match')).toBeInTheDocument(); 32 | }); 33 | 34 | it('displays success message on successful signup', async () => { 35 | render(); 36 | fireEvent.change(screen.getByPlaceholderText('Email'), { target: { value: 'testuser' } }); 37 | fireEvent.change(screen.getByPlaceholderText('Password'), { target: { value: 'password123' } }); 38 | fireEvent.change(screen.getByPlaceholderText('Confirm Password'), { target: { value: 'password123' } }); 39 | 40 | global.fetch = jest.fn(() => 41 | Promise.resolve({ 42 | ok: true, 43 | json: () => Promise.resolve({}), 44 | }) 45 | ); 46 | 47 | signIn.mockReturnValue(Promise.resolve({ ok: true })); 48 | 49 | fireEvent.click(screen.getByRole('button', { name: /Sign Up/i })); 50 | 51 | expect(await screen.findByText('User registered successfully.')).toBeInTheDocument(); 52 | expect(global.fetch).toHaveBeenCalledWith('/api/signup', expect.anything()); 53 | expect(signIn).toHaveBeenCalledWith('credentials', expect.anything()); 54 | }); 55 | 56 | it('displays error message on signup failure', async () => { 57 | render(); 58 | fireEvent.change(screen.getByPlaceholderText('Email'), { target: { value: 'testuser' } }); 59 | fireEvent.change(screen.getByPlaceholderText('Password'), { target: { value: 'password123' } }); 60 | fireEvent.change(screen.getByPlaceholderText('Confirm Password'), { target: { value: 'password123' } }); 61 | 62 | global.fetch = jest.fn(() => 63 | Promise.resolve({ 64 | ok: false, 65 | json: () => Promise.resolve({ message: 'Signup failed' }), 66 | }) 67 | ); 68 | 69 | fireEvent.click(screen.getByRole('button', { name: /Sign Up/i })); 70 | 71 | expect(await screen.findByText('Signup failed')).toBeInTheDocument(); 72 | }); 73 | 74 | it('calls signIn on OAuth button click', async () => { 75 | render(); 76 | 77 | signIn.mockReturnValue(Promise.resolve({ error: null })); 78 | 79 | fireEvent.click(screen.getAllByRole('button')[1]); // Assuming the first button is the signup button and the second is the Google button 80 | 81 | await waitFor(() => { 82 | expect(signIn).toHaveBeenCalledWith('google', { redirect: false }); 83 | }); 84 | 85 | fireEvent.click(screen.getAllByRole('button')[2]); // Assuming the third button is the GitHub button 86 | 87 | await waitFor(() => { 88 | expect(signIn).toHaveBeenCalledWith('github', { redirect: false }); 89 | }); 90 | }); 91 | 92 | it('applies correct styles on mount and cleans up on unmount', () => { 93 | const { unmount } = render(); 94 | const bodyStyles = document.body.style; 95 | 96 | expect(bodyStyles.fontFamily).toBe("'Poppins', sans-serif"); 97 | expect(bodyStyles.display).toBe('flex'); 98 | expect(bodyStyles.justifyContent).toBe('center'); 99 | expect(bodyStyles.alignItems).toBe('center'); 100 | expect(bodyStyles.minHeight).toBe('100vh'); 101 | expect(bodyStyles.backgroundImage).toBe('url(https://getwallpapers.com/wallpaper/full/2/8/f/537844.jpg)'); 102 | expect(bodyStyles.backgroundRepeat).toBe('no-repeat'); 103 | expect(bodyStyles.backgroundSize).toBe('cover'); 104 | expect(bodyStyles.backgroundPosition).toBe('center'); 105 | 106 | unmount(); 107 | 108 | expect(bodyStyles.fontFamily).toBe(''); 109 | expect(bodyStyles.display).toBe(''); 110 | expect(bodyStyles.justifyContent).toBe(''); 111 | expect(bodyStyles.alignItems).toBe(''); 112 | expect(bodyStyles.minHeight).toBe(''); 113 | expect(bodyStyles.backgroundImage).toBe(''); 114 | expect(bodyStyles.backgroundRepeat).toBe('repeat'); 115 | expect(bodyStyles.backgroundSize).toBe('auto'); 116 | expect(bodyStyles.backgroundPosition).toBe('0% 0%'); 117 | }); 118 | }); 119 | 120 | 121 | -------------------------------------------------------------------------------- /data.json: -------------------------------------------------------------------------------- 1 | {"testData":[]} -------------------------------------------------------------------------------- /dataBuild.json: -------------------------------------------------------------------------------- 1 | {"testData":[{"buildTime":5482},{"buildTime":16941},{"buildTime":9207},{"buildTime":5564},{"buildTime":5936},{"buildTime":6563},{"buildTime":10520,"apiKey":"1234567890"},{"buildTime":"201","apiKey":"aowifhojbefkw"},{"buildTime":"201","apiKey":"aowifhojbefkw"},{"buildTime":"201","apiKey":"aowifhojbefkw"},{"buildTime":"202","apiKey":"plsworkk!!!!"},{"buildTime":"202","apiKey":"plsworkk!!!!"},{"buildTime":"24230","apiKey":"plsworkk!!!!"},{"buildTime":5500,"apiKey":"1234567890"},{"buildTime":"24230","apiKey":"plsworkk!!!!"}]} -------------------------------------------------------------------------------- /dataBundle.json: -------------------------------------------------------------------------------- 1 | {"testData":[{"bundleLog":" ▲ Next.js 14.2.4\n - Environments: .env.local\n\n Creating an optimized production build ...\nWebpack Bundle Analyzer saved report to /Users/nellysegimoto/Codesmith/dummyNextApp/.next/analyze/nodejs.html\n\nNo bundles were parsed. Analyzer will show only original module sizes from stats file.\n\nWebpack Bundle Analyzer saved report to /Users/nellysegimoto/Codesmith/dummyNextApp/.next/analyze/edge.html\nWebpack Bundle Analyzer saved report to /Users/nellysegimoto/Codesmith/dummyNextApp/.next/analyze/client.html\n ✓ Compiled successfully\n Linting and checking validity of types ...\n Collecting page data ...\n Generating static pages (0/5) ...\n Generating static pages (1/5) \r\n Generating static pages (2/5) \r\n Generating static pages (3/5) \r\n ✓ Generating static pages (5/5)\n Finalizing page optimization ...\n Collecting build traces ...\n\nRoute (app) Size First Load JS\n┌ ○ / 5.42 kB 92.4 kB\n└ ○ /_not-found 871 B 87.9 kB\n+ First Load JS shared by all 87 kB\n ├ chunks/23-ef3c75ca91144cad.js 31.5 kB\n ├ chunks/fd9d1056-2821b0f0cabcd8bd.js 53.6 kB\n └ other shared chunks (total) 1.87 kB\n\n\n○ (Static) prerendered as static content\n\n"},{"bundleLog":" ▲ Next.js 14.2.4\n - Environments: .env.local\n\n Creating an optimized production build ...\nWebpack Bundle Analyzer saved report to /Users/nellysegimoto/Codesmith/dummyNextApp/.next/analyze/nodejs.html\n\nNo bundles were parsed. Analyzer will show only original module sizes from stats file.\n\nWebpack Bundle Analyzer saved report to /Users/nellysegimoto/Codesmith/dummyNextApp/.next/analyze/edge.html\nWebpack Bundle Analyzer saved report to /Users/nellysegimoto/Codesmith/dummyNextApp/.next/analyze/client.html\n ✓ Compiled successfully\n Linting and checking validity of types ...\n Collecting page data ...\n Generating static pages (0/5) ...\n Generating static pages (1/5) \r\n Generating static pages (2/5) \r\n Generating static pages (3/5) \r\n ✓ Generating static pages (5/5)\n Finalizing page optimization ...\n Collecting build traces ...\n\nRoute (app) Size First Load JS\n┌ ○ / 5.42 kB 92.5 kB\n└ ○ /_not-found 871 B 87.9 kB\n+ First Load JS shared by all 87.1 kB\n ├ chunks/23-ef3c75ca91144cad.js 31.5 kB\n ├ chunks/fd9d1056-2821b0f0cabcd8bd.js 53.6 kB\n └ other shared chunks (total) 1.9 kB\n\n\n○ (Static) prerendered as static content\n\n"},{"bundleLog":" ▲ Next.js 14.2.4\n - Environments: .env.local\n\n Creating an optimized production build ...\nWebpack Bundle Analyzer saved report to /Users/nellysegimoto/Codesmith/dummyNextApp/.next/analyze/nodejs.html\n\nNo bundles were parsed. Analyzer will show only original module sizes from stats file.\n\nWebpack Bundle Analyzer saved report to /Users/nellysegimoto/Codesmith/dummyNextApp/.next/analyze/edge.html\nWebpack Bundle Analyzer saved report to /Users/nellysegimoto/Codesmith/dummyNextApp/.next/analyze/client.html\n ✓ Compiled successfully\n Linting and checking validity of types ...\n Collecting page data ...\n Generating static pages (0/5) ...\n Generating static pages (1/5) \r\n Generating static pages (2/5) \r\n Generating static pages (3/5) \r\n ✓ Generating static pages (5/5)\n Finalizing page optimization ...\n Collecting build traces ...\n\nRoute (app) Size First Load JS\n┌ ○ / 5.42 kB 92.5 kB\n└ ○ /_not-found 871 B 87.9 kB\n+ First Load JS shared by all 87.1 kB\n ├ chunks/23-ef3c75ca91144cad.js 31.5 kB\n ├ chunks/fd9d1056-2821b0f0cabcd8bd.js 53.6 kB\n └ other shared chunks (total) 1.9 kB\n\n\n○ (Static) prerendered as static content\n\n"}]} -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const nextJest = require('next/jest') 2 | 3 | /** @type {import('jest').Config} */ 4 | const createJestConfig = nextJest({ 5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 6 | dir: './', 7 | }) 8 | 9 | // Add any custom config to be passed to Jest 10 | const config = { 11 | coverageProvider: 'v8', 12 | testEnvironment: 'jsdom', 13 | // Add more setup options before each test is run 14 | // setupFilesAfterEnv: ['/jest.setup.ts'], 15 | } 16 | 17 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 18 | module.exports = createJestConfig(config) -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "paths": { 5 | "@/*": ["./src/*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import pkg from '@next/bundle-analyzer'; 2 | const withBundleAnalyzer = pkg({ 3 | enabled: process.env.ANALYZE === 'true', 4 | }); 5 | 6 | /** @type {import('next').NextConfig} */ 7 | 8 | const nextConfig = { 9 | experimental: { 10 | instrumentationHook: true, 11 | }, 12 | async headers() { 13 | return [ 14 | { 15 | // matching all API routes 16 | source: "/dashboard/api/:path*", 17 | headers: [ 18 | { key: "Access-Control-Allow-Credentials", value: "true" }, 19 | { key: "Access-Control-Allow-Origin", value: "*" }, 20 | { key: "Access-Control-Allow-Methods", value: "GET,DELETE,PATCH,POST,PUT" }, 21 | { key: "Access-Control-Allow-Headers", value: "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Connection" }, 22 | ] 23 | } 24 | ] 25 | } 26 | }; 27 | 28 | export default withBundleAnalyzer(nextConfig); -------------------------------------------------------------------------------- /nextlevelpackage/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /nextlevelpackage/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | 1. To get started, visit nextlevel-dash.com and create an account. After account creation, you will be navigated to an onboardingp page which will direct you to complete the below steps. 4 | 5 | 2. Install configure next.js bundle analyzer in your Next.js application 6 | 7 | NPM install Next.js Bundle Analyzer: 8 | ```bash 9 | npm install @next/bundle-analyzer 10 | ``` 11 | 12 | Configure next.config.mjs file: 13 | ```bash 14 | import pkg from '@next/bundle-analyzer'; 15 | const withBundleAnalyzer = pkg({ 16 | enabled: process.env.ANALYZE === 'true', 17 | }); 18 | 19 | const nextConfig = {}; 20 | 21 | export default withBundleAnalyzer(nextConfig); 22 | ``` 23 | 24 | 3. Install and configure our npm package, NextLevelPackage, through the terminal. It can also be found [here](https://www.npmjs.com/package/nextlevelpackage). 25 | 26 | NPM Install NextLevelPackage: 27 | ```bash 28 | npm install nextlevelpackage 29 | ``` 30 | 31 | Import NextLevelPackage in layout.js: 32 | ```bash 33 | import NextWebVitals from 'nextlevelpackage'; 34 | ``` 35 | 36 | Add NextWebVitals component in RootLayout body: 37 | ```bash 38 | export default function RootLayout({ children }) { 39 | return ( 40 | 41 | 42 | 43 | {children} 44 | 45 | 46 | ); 47 | } 48 | ``` 49 | 50 | 4. Configure yoour Environmental Variables 51 | 52 | Add the following line to your .env.local file: 53 | ```bash 54 | NEXT_PUBLIC_API_KEY= 55 | ``` 56 | When you create an account, the setup page will provide you with your API key. 57 | 58 | 5. Add Build Script to package.json 59 | 60 | Add the following script to your package.json: 61 | ```bash 62 | "scripts": { 63 | "nextlevelbuild": "node ./node_modules/nextlevelpackage/cli.js" 64 | } 65 | ``` 66 | 67 | Run this build script instead of 'npm next build' to track metrics in the dashboard: 68 | ```bash 69 | npm run nextlevelbuild 70 | ``` 71 | 72 | 6. Navitage to your NextLevel dashboard to view tracked metrics! 73 | 74 | ## Tracked Metrics 75 | 76 | Metrics displayed on the page include: 77 | 78 | - Time to First Byte: 79 | - measures the time taken from a user's request to the first byte of data received from the server, indicating server responsiveness. 80 | - Largest Contentful Paint: 81 | - gauges the time it takes for the largest visible content element on a webpage to load, impacting user perception of loading speed. 82 | - First Contentful Paint: 83 | - tracks the time from page load to when the first piece of content is rendered on the screen, marking the start of the visual loading process. 84 | - First Input Delay: 85 | - measures the delay between the user's first interaction with the page and the browser's response, reflecting input responsiveness. 86 | - Interaction to Next Paint: 87 | - evaluates the time from user interaction to the next visual update, assessing how quickly a page responds to user inputs. 88 | - Cumulative Layout Shift: 89 | - quantifies the total of all individual layout shifts that occur during the entire lifespan of the page, indicating visual stability. 90 | - Build Time: 91 | - refers to the duration taken to compile and bundle code and assets into a deployable format during the development process. 92 | - Bundle Size: 93 | - denotes the total size of all the compiled assets sent to the client, affecting load times and overall performance. -------------------------------------------------------------------------------- /nextlevelpackage/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // makes this the entry point for command line argument 3 | 4 | const { sendToApi } = require('./sendToApi'); 5 | const { spawn } = require('child_process'); 6 | 7 | const bundleBuildAnalyzer = () => { 8 | 9 | const buildURL = `https://www.nextlevel-dash.com/dashboard/api/build`; 10 | const bundleURL = `https://www.nextlevel-dash.com/dashboard/api/bundle`; 11 | 12 | const command = 'next'; 13 | const args = ['build']; 14 | const env = { ...process.env, ANALYZE: 'true' }; 15 | const startTime = Date.now(); // start time before execution 16 | 17 | const buildProcess = spawn(command, args, { env }); 18 | 19 | let bundleLog = ''; // capture bundle analyzer metrics 20 | 21 | buildProcess.stdout.on('data', (data) => { 22 | console.log(data.toString()); 23 | bundleLog += data.toString(); 24 | }); 25 | 26 | // if error in the build process 27 | buildProcess.stderr.on('data', (data) => { 28 | console.error(data.toString()); 29 | }); 30 | 31 | buildProcess.on('close', (code) => { 32 | // use at the end of build process, if code != 0 then there was an error 33 | if (code !== 0) { 34 | console.error(`Build process exited with code ${code}`); 35 | return; 36 | } 37 | const endTime = Date.now(); // End time after execution has finished 38 | const buildTime = endTime - startTime; 39 | console.log('Build time:', buildTime); 40 | sendToApi({ buildTime }, buildURL); 41 | 42 | //sendinig whole bundle log to api 43 | sendToApi({ bundleLog }, bundleURL); 44 | }); 45 | } 46 | 47 | bundleBuildAnalyzer(); -------------------------------------------------------------------------------- /nextlevelpackage/index.js: -------------------------------------------------------------------------------- 1 | //index.js 2 | 'use client' 3 | 4 | import { useReportWebVitals } from 'next/web-vitals' 5 | 6 | 7 | export default function NextWebVitals() { 8 | useReportWebVitals((metric) => { 9 | if (!process.env.NEXT_PUBLIC_API_KEY) { 10 | console.log('API key not found in environment variables'); 11 | return; 12 | } 13 | const apiKey = process.env.NEXT_PUBLIC_API_KEY; 14 | const data = { 15 | "metricType": metric.name, 16 | "metricValue": metric.value, 17 | apiKey}; 18 | console.log('Web vitals data:', data); 19 | const body = JSON.stringify(data); 20 | const url = 'https://www.nextlevel-dash.com/dashboard/api/webvitals'; 21 | 22 | fetch(url, { 23 | method: 'POST', 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | }, 27 | body, 28 | keepalive: true 29 | }).then((res) => { 30 | if (res.ok) { 31 | console.log('Web vitals data sent successfully'); 32 | } else { 33 | console.error('index.js .then error Error sending web vitals data:', res.statusText); 34 | } 35 | }).catch((error) => { 36 | console.error('index.js .catch error Error sending web vitals data:', error); 37 | }) 38 | }) 39 | } -------------------------------------------------------------------------------- /nextlevelpackage/node_modules/.package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextlevelpackage", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "node_modules/data-uri-to-buffer": { 8 | "version": "4.0.1", 9 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", 10 | "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", 11 | "license": "MIT", 12 | "engines": { 13 | "node": ">= 12" 14 | } 15 | }, 16 | "node_modules/dotenv": { 17 | "version": "16.4.5", 18 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", 19 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", 20 | "license": "BSD-2-Clause", 21 | "engines": { 22 | "node": ">=12" 23 | }, 24 | "funding": { 25 | "url": "https://dotenvx.com" 26 | } 27 | }, 28 | "node_modules/fetch-blob": { 29 | "version": "3.2.0", 30 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", 31 | "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", 32 | "funding": [ 33 | { 34 | "type": "github", 35 | "url": "https://github.com/sponsors/jimmywarting" 36 | }, 37 | { 38 | "type": "paypal", 39 | "url": "https://paypal.me/jimmywarting" 40 | } 41 | ], 42 | "license": "MIT", 43 | "dependencies": { 44 | "node-domexception": "^1.0.0", 45 | "web-streams-polyfill": "^3.0.3" 46 | }, 47 | "engines": { 48 | "node": "^12.20 || >= 14.13" 49 | } 50 | }, 51 | "node_modules/formdata-polyfill": { 52 | "version": "4.0.10", 53 | "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", 54 | "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", 55 | "license": "MIT", 56 | "dependencies": { 57 | "fetch-blob": "^3.1.2" 58 | }, 59 | "engines": { 60 | "node": ">=12.20.0" 61 | } 62 | }, 63 | "node_modules/node-domexception": { 64 | "version": "1.0.0", 65 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 66 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", 67 | "funding": [ 68 | { 69 | "type": "github", 70 | "url": "https://github.com/sponsors/jimmywarting" 71 | }, 72 | { 73 | "type": "github", 74 | "url": "https://paypal.me/jimmywarting" 75 | } 76 | ], 77 | "license": "MIT", 78 | "engines": { 79 | "node": ">=10.5.0" 80 | } 81 | }, 82 | "node_modules/node-fetch": { 83 | "version": "3.3.2", 84 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", 85 | "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", 86 | "license": "MIT", 87 | "dependencies": { 88 | "data-uri-to-buffer": "^4.0.0", 89 | "fetch-blob": "^3.1.4", 90 | "formdata-polyfill": "^4.0.10" 91 | }, 92 | "engines": { 93 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 94 | }, 95 | "funding": { 96 | "type": "opencollective", 97 | "url": "https://opencollective.com/node-fetch" 98 | } 99 | }, 100 | "node_modules/web-streams-polyfill": { 101 | "version": "3.3.3", 102 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", 103 | "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", 104 | "license": "MIT", 105 | "engines": { 106 | "node": ">= 8" 107 | } 108 | }, 109 | "node_modules/web-vitals": { 110 | "version": "4.2.1", 111 | "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.1.tgz", 112 | "integrity": "sha512-U6bAxeudnhDqcXNl50JC4hLlqox9DZnngxfISZm3DMZnonW35xtJOVUc091L+DOY+6hVZVpKXoiCP0RiT6339Q==", 113 | "license": "Apache-2.0" 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /nextlevelpackage/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextlevelpackage", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "nextlevelpackage", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "dotenv": "^16.4.5", 13 | "node-fetch": "^3.3.2", 14 | "web-vitals": "^4.2.1" 15 | } 16 | }, 17 | "node_modules/data-uri-to-buffer": { 18 | "version": "4.0.1", 19 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", 20 | "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", 21 | "license": "MIT", 22 | "engines": { 23 | "node": ">= 12" 24 | } 25 | }, 26 | "node_modules/dotenv": { 27 | "version": "16.4.5", 28 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", 29 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", 30 | "license": "BSD-2-Clause", 31 | "engines": { 32 | "node": ">=12" 33 | }, 34 | "funding": { 35 | "url": "https://dotenvx.com" 36 | } 37 | }, 38 | "node_modules/fetch-blob": { 39 | "version": "3.2.0", 40 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", 41 | "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", 42 | "funding": [ 43 | { 44 | "type": "github", 45 | "url": "https://github.com/sponsors/jimmywarting" 46 | }, 47 | { 48 | "type": "paypal", 49 | "url": "https://paypal.me/jimmywarting" 50 | } 51 | ], 52 | "license": "MIT", 53 | "dependencies": { 54 | "node-domexception": "^1.0.0", 55 | "web-streams-polyfill": "^3.0.3" 56 | }, 57 | "engines": { 58 | "node": "^12.20 || >= 14.13" 59 | } 60 | }, 61 | "node_modules/formdata-polyfill": { 62 | "version": "4.0.10", 63 | "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", 64 | "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", 65 | "license": "MIT", 66 | "dependencies": { 67 | "fetch-blob": "^3.1.2" 68 | }, 69 | "engines": { 70 | "node": ">=12.20.0" 71 | } 72 | }, 73 | "node_modules/node-domexception": { 74 | "version": "1.0.0", 75 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 76 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", 77 | "funding": [ 78 | { 79 | "type": "github", 80 | "url": "https://github.com/sponsors/jimmywarting" 81 | }, 82 | { 83 | "type": "github", 84 | "url": "https://paypal.me/jimmywarting" 85 | } 86 | ], 87 | "license": "MIT", 88 | "engines": { 89 | "node": ">=10.5.0" 90 | } 91 | }, 92 | "node_modules/node-fetch": { 93 | "version": "3.3.2", 94 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", 95 | "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", 96 | "license": "MIT", 97 | "dependencies": { 98 | "data-uri-to-buffer": "^4.0.0", 99 | "fetch-blob": "^3.1.4", 100 | "formdata-polyfill": "^4.0.10" 101 | }, 102 | "engines": { 103 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 104 | }, 105 | "funding": { 106 | "type": "opencollective", 107 | "url": "https://opencollective.com/node-fetch" 108 | } 109 | }, 110 | "node_modules/web-streams-polyfill": { 111 | "version": "3.3.3", 112 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", 113 | "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", 114 | "license": "MIT", 115 | "engines": { 116 | "node": ">= 8" 117 | } 118 | }, 119 | "node_modules/web-vitals": { 120 | "version": "4.2.1", 121 | "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.1.tgz", 122 | "integrity": "sha512-U6bAxeudnhDqcXNl50JC4hLlqox9DZnngxfISZm3DMZnonW35xtJOVUc091L+DOY+6hVZVpKXoiCP0RiT6339Q==", 123 | "license": "Apache-2.0" 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /nextlevelpackage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextlevelpackage", 3 | "version": "1.0.7", 4 | "description": "this package will connect you to the nextlevel dashboard", 5 | "main": "index.js", 6 | "scripts": { 7 | "nextLevelAnalyze": "node buildBundleAnalyzer.js" 8 | }, 9 | "keywords": [ 10 | "next.js", 11 | "performance-metrics", 12 | "web-vitals", 13 | "build-time", 14 | "bundle-analyzer" 15 | ], 16 | "author": "Kim Cuomo, Fredrico Aires da Neto, Ian Mann, Nelly Segimoto", 17 | "license": "ISC", 18 | "dependencies": { 19 | "dotenv": "^16.4.5", 20 | "node-fetch": "^3.3.2", 21 | "web-vitals": "^4.2.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /nextlevelpackage/sendToApi.js: -------------------------------------------------------------------------------- 1 | const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); 2 | require('dotenv').config({ path: '.env.local' }); 3 | 4 | 5 | // creating a sendToApi function that will send data to different endpoints 6 | // endpoint for bundle analyzer data, endpoint for build time data 7 | const sendToApi = (data, url) => { 8 | // check if api key is in the environment variables 9 | if (!process.env.NEXT_PUBLIC_API_KEY) { 10 | console.log('API key not found in environment variables'); 11 | return; 12 | } 13 | const apiKey = process.env.NEXT_PUBLIC_API_KEY; 14 | const body = JSON.stringify(data); 15 | fetch(url, { 16 | method: 'POST', 17 | body, 18 | headers: { 19 | 'Content-Type': 'application/json', 20 | 'Authorization': apiKey 21 | }, 22 | keepalive: true 23 | }).catch(err => { 24 | console.log('Error sending data to server:', err); 25 | }); 26 | } 27 | 28 | module.exports = { sendToApi }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextlevel", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "nextlevelbuild": "node ./node_modules/nextlevelpackage/cli.js", 11 | "test": "jest", 12 | "test:watch": "jest --watch" 13 | }, 14 | "dependencies": { 15 | "@next/bundle-analyzer": "^14.2.5", 16 | "@supercharge/strings": "^2.0.0", 17 | "bcrypt": "^5.1.1", 18 | "bcryptjs": "^2.4.3", 19 | "chart.js": "^4.4.3", 20 | "chartjs-adapter-date-fns": "^3.0.0", 21 | "date-fns": "^3.6.0", 22 | "geist": "^1.3.1", 23 | "mongodb": "^6.8.0", 24 | "mongoose": "^8.5.1", 25 | "next": "^14.2.14", 26 | "next-auth": "^4.24.7", 27 | "nextjs-cors": "^2.2.0", 28 | "nextlevelpackage": "^1.0.4", 29 | "nodemon": "^3.1.4", 30 | "normalize.css": "^8.0.1", 31 | "react": "^18", 32 | "react-chartjs-2": "^5.2.0", 33 | "react-dom": "^18", 34 | "react-icons": "^5.2.1", 35 | "sharp": "^0.33.4", 36 | "web-vitals": "^4.2.1" 37 | }, 38 | "devDependencies": { 39 | "@testing-library/jest-dom": "^6.4.7", 40 | "@testing-library/react": "^16.0.0", 41 | "@types/node": "^20.14.10", 42 | "@types/react": "18.3.3", 43 | "jest": "^29.7.0", 44 | "jest-environment-jsdom": "^29.7.0", 45 | "typescript": "5.5.3" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/HEADSHOTS/fred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/HEADSHOTS/fred.png -------------------------------------------------------------------------------- /public/HEADSHOTS/fredBW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/HEADSHOTS/fredBW.png -------------------------------------------------------------------------------- /public/HEADSHOTS/ian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/HEADSHOTS/ian.png -------------------------------------------------------------------------------- /public/HEADSHOTS/ianBW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/HEADSHOTS/ianBW.png -------------------------------------------------------------------------------- /public/HEADSHOTS/kim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/HEADSHOTS/kim.png -------------------------------------------------------------------------------- /public/HEADSHOTS/kimBW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/HEADSHOTS/kimBW.png -------------------------------------------------------------------------------- /public/HEADSHOTS/nelly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/HEADSHOTS/nelly.png -------------------------------------------------------------------------------- /public/HEADSHOTS/nellyBW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/HEADSHOTS/nellyBW.png -------------------------------------------------------------------------------- /public/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/Icon.png -------------------------------------------------------------------------------- /public/LOADINGLOGO.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/LOADINGLOGO.mp4 -------------------------------------------------------------------------------- /public/METRICS/fid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/METRICS/inp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/METRICS/ttfb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/NEXTLEVEL.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/NEXTLEVEL.mp4 -------------------------------------------------------------------------------- /public/NEXTLEVELANI.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/NEXTLEVELANI.mp4 -------------------------------------------------------------------------------- /public/NextLevelBanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/NextLevelBanner.png -------------------------------------------------------------------------------- /public/NextLevelDashboard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/NextLevelDashboard.gif -------------------------------------------------------------------------------- /public/NextLevelDashboardHomepage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/NextLevelDashboardHomepage.gif -------------------------------------------------------------------------------- /public/NextLevelGifHomepage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/NextLevelGifHomepage.gif -------------------------------------------------------------------------------- /public/TopNavLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/TopNavLogo.png -------------------------------------------------------------------------------- /public/TransparentGifLogo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/TransparentGifLogo.gif -------------------------------------------------------------------------------- /public/TransparentIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/TransparentIcon.png -------------------------------------------------------------------------------- /public/TransparentLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/TransparentLogo.png -------------------------------------------------------------------------------- /public/TransparentLogoLessSpace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/TransparentLogoLessSpace.png -------------------------------------------------------------------------------- /public/UpdatedNextLevelGifHomepage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/UpdatedNextLevelGifHomepage.gif -------------------------------------------------------------------------------- /public/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/github.png -------------------------------------------------------------------------------- /public/laptop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/laptop.png -------------------------------------------------------------------------------- /public/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/public/linkedin.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/transparent-gif-v2-_1_.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/callback/[provider].js: -------------------------------------------------------------------------------- 1 | import { getSession } from 'next-auth/react'; 2 | 3 | export default async function handler(req, res) { 4 | const session = await getSession({ req }); 5 | 6 | if (!session) { 7 | res.redirect('/login'); 8 | return; 9 | } 10 | 11 | const username = session.user.email; // Assuming the OAuth provider returns a `name` field 12 | 13 | // Redirect to the onboarding page with the username 14 | res.redirect(`/onboarding?username=${encodeURIComponent(username)}`); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.js: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth'; 2 | import GithubProvider from 'next-auth/providers/github'; 3 | import GoogleProvider from 'next-auth/providers/google'; 4 | import CredentialsProvider from 'next-auth/providers/credentials'; 5 | import { verifyCredentials } from '../../../lib/auth'; // Adjust the path to your auth function 6 | import dbConnect from '../../../lib/connectDB'; 7 | import User from '../../../models/User'; 8 | import Str from '@supercharge/strings'; 9 | 10 | const handler = NextAuth({ 11 | providers: [ 12 | GithubProvider({ 13 | clientId: process.env.GITHUB_ID, 14 | clientSecret: process.env.GITHUB_SECRET, 15 | }), 16 | GoogleProvider({ 17 | clientId: process.env.GOOGLE_CLIENT_ID, 18 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 19 | }), 20 | CredentialsProvider({ 21 | name: 'Credentials', 22 | credentials: { 23 | username: { label: 'Username', type: 'text' }, 24 | password: { label: 'Password', type: 'password' }, 25 | }, 26 | authorize: async (credentials) => { 27 | console.log('Authorizing user with credentials:', credentials); 28 | const user = await verifyCredentials(credentials.username, credentials.password); 29 | if (user) { 30 | console.log('User authorized:', user); 31 | return user; 32 | } else { 33 | console.log('Authorization failed'); 34 | throw new Error('Invalid username or password'); 35 | } 36 | }, 37 | }), 38 | ], 39 | secret: process.env.NEXTAUTH_SECRET, 40 | session: { 41 | jwt: true, 42 | }, 43 | callbacks: { 44 | async signIn({ user, account, profile }) { 45 | console.log('SignIn callback:', user, account, profile); 46 | await dbConnect(); 47 | 48 | try { 49 | const existingUser = await User.findOne({ email: user.email }); 50 | 51 | if (!existingUser) { 52 | let APIkey = Str.random(); //generating random API key 53 | 54 | let existingAPI = await User.findOne({ APIkey }); 55 | 56 | while (existingAPI) { 57 | APIkey = Str.random(); 58 | existingAPI = await User.findOne({ APIkey }); 59 | } 60 | const newUser = new User({ 61 | username: user.email, 62 | email: user.email, 63 | APIkey, 64 | }); 65 | await newUser.save(); 66 | } 67 | 68 | return true; 69 | } catch (error) { 70 | console.error('Error in signIn callback:', error); 71 | return false; 72 | } 73 | }, 74 | async session({ session, token }) { 75 | // console.log('Session callback:', session, token); 76 | session.user = token.user; 77 | return session; 78 | }, 79 | async jwt({ token, user }) { 80 | // console.log('JWT callback:', token, user); 81 | if (user) { 82 | token.user = user; 83 | } 84 | return token; 85 | }, 86 | }, 87 | events: { 88 | error: (message) => { 89 | console.error('NextAuth error:', message); 90 | }, 91 | }, 92 | }); 93 | 94 | export { handler as GET, handler as POST }; 95 | 96 | -------------------------------------------------------------------------------- /src/app/api/login/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import bcrypt from 'bcryptjs'; 3 | import dbConnect from '../../lib/connectDB'; 4 | import User from '../../models/User'; 5 | 6 | export async function POST(req) { 7 | const { username, password } = await req.json(); 8 | 9 | if (!username || !password) { 10 | return NextResponse.json( 11 | { message: 'Missing required fields' }, 12 | { status: 400 } 13 | ); 14 | } 15 | 16 | await dbConnect(); 17 | 18 | try { 19 | const user = await User.findOne({ username }); 20 | 21 | if (!user) { 22 | return NextResponse.json( 23 | { message: 'Invalid username or password' }, 24 | { status: 401 } 25 | ); 26 | } 27 | 28 | const isPassValid = await bcrypt.compare(password, user.password); 29 | 30 | if (!isPassValid) { 31 | return NextResponse.json( 32 | { message: 'Invalid username or password' }, 33 | { status: 401 } 34 | ); 35 | } 36 | 37 | return NextResponse.json( 38 | { message: 'Login successful. Welcome ', user }, 39 | { status: 200 } 40 | ); 41 | } catch (error) { 42 | console.log(error); 43 | return NextResponse.json( 44 | { message: 'Internal server error' }, 45 | { status: 500 } 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/api/signup/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import bcrypt from 'bcryptjs'; 3 | import dbConnect from '../../lib/connectDB'; 4 | import User from '../../models/User'; 5 | import Str from '@supercharge/strings'; 6 | 7 | 8 | export async function POST(req) { 9 | const { username, email, password } = await req.json(); 10 | 11 | if (!username || !password || !email ) { 12 | return NextResponse.json({ message: 'Missing required fields' }, { status: 400 }); 13 | } 14 | 15 | await dbConnect(); 16 | 17 | try { 18 | const existingUser = await User.findOne({ email }); 19 | 20 | if (existingUser) { //if user already exsists 21 | return NextResponse.json({ message: 'User already exists' }, { status: 409 }); 22 | } 23 | const hashedPassword = await bcrypt.hash(password, 10); //hashiing the password 24 | 25 | let APIkey = Str.random(); //generating random API key 26 | 27 | let existingAPI = await User.findOne({ APIkey }); 28 | 29 | while (existingAPI) { 30 | APIkey = Str.random(); 31 | existingAPI = await User.findOne({ APIkey }); 32 | } 33 | 34 | const user = new User({ //creating the new user 35 | username, 36 | email, 37 | password: hashedPassword, 38 | APIkey, 39 | }); 40 | 41 | await user.save();//saving user to the database 42 | 43 | return NextResponse.json({ message: 'User created successfully', user }, { status: 201 }); 44 | } catch (error) { 45 | console.error(error); 46 | return NextResponse.json({ message: 'Internal server error' }, { status: 500 }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/components/Modal.css: -------------------------------------------------------------------------------- 1 | .modal-overlay { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | background: rgba(0, 0, 0, 0.5); 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | } 12 | 13 | .modal-content { 14 | background: white; 15 | padding: 2rem; 16 | border-radius: 8px; 17 | text-align: center; 18 | } 19 | 20 | .oauth-button { 21 | background-color: #ffffff; 22 | border: 1px solid #dddddd; 23 | padding: 7px; 24 | padding-left: 15px; 25 | gap: 100px !important; 26 | display: inline-block; 27 | width: 140px !important; 28 | align-items: center; 29 | justify-content: center; 30 | cursor: pointer; 31 | font-size: 16px; 32 | color: #333; 33 | margin-right: 10px; 34 | margin-left: 10px; 35 | } 36 | 37 | .github-icon { 38 | margin-right: 8px; 39 | font-size: 30px; 40 | } 41 | 42 | .google-icon{ 43 | margin-right: 8px; 44 | font-size: 30px; 45 | } 46 | .close-button { 47 | margin-top: 1rem; 48 | padding: 0.5rem 1rem; 49 | cursor: pointer; 50 | } 51 | -------------------------------------------------------------------------------- /src/app/components/Modal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Modal.css'; // Create and import modal specific styles 3 | import { FcGoogle } from 'react-icons/fc'; 4 | import { DiGithubBadge } from 'react-icons/di'; 5 | 6 | export default function Modal({ isOpen, onClose, handleOAuthSignIn }) { 7 | if (!isOpen) return null; 8 | 9 | return ( 10 |
11 |
12 |

Login with

13 | 16 | 19 | 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/components/SessionWrapper.tsx: -------------------------------------------------------------------------------- 1 | // mark as client component 2 | "use client"; 3 | import { SessionProvider } from "next-auth/react"; 4 | import React from 'react'; 5 | 6 | const SessionWrapper = ({ children }) => { 7 | return ( 8 | 9 | {children} 10 | 11 | ); 12 | } 13 | 14 | export default SessionWrapper; 15 | -------------------------------------------------------------------------------- /src/app/components/Spinner.js: -------------------------------------------------------------------------------- 1 | // Spinner.js 2 | import React from 'react'; 3 | import './Spinner.module.css'; 4 | 5 | export default function Spinner() { 6 | return ( 7 |
8 |
9 | 13 |
14 |
15 | ); 16 | } -------------------------------------------------------------------------------- /src/app/components/Spinner.module.css: -------------------------------------------------------------------------------- 1 | /* spinner.css */ 2 | .spinner-overlay { 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | background: rgba(0, 0, 0, 0.5); 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | z-index: 1000; 13 | } 14 | 15 | .spinner-container { 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | } 20 | 21 | .spinner-video { 22 | width: 100px; /* Set to a smaller size */ 23 | height: 100px; /* Set to a smaller size */ 24 | object-fit: contain; /* Ensure the video scales properly */ 25 | } 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/app/components/topnav.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import styles from './topnav.module.css'; 5 | import Link from 'next/link'; 6 | import Image from 'next/image'; 7 | import logo from '/public/TransparentLogoLessSpace.png'; 8 | import { useSession, signOut } from 'next-auth/react'; 9 | 10 | function TopNav() { 11 | const { data: session, status } = useSession(); 12 | 13 | const handleLogout = async () => { 14 | await signOut({ callbackUrl: '/login' }); 15 | }; 16 | 17 | return ( 18 |
19 | 38 |
39 | ); 40 | } 41 | 42 | export default TopNav; 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/app/components/topnav.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | background-color: hsl(0, 0%, 100%) !important; 7 | padding: 0px 20px; 8 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 9 | z-index: 1000; /* Ensure it appears above other content */ 10 | } 11 | 12 | .nav { 13 | display: flex; 14 | justify-content: space-between; 15 | align-items: center; 16 | height: 75px; 17 | flex-wrap: nowrap; 18 | overflow: hidden; 19 | } 20 | 21 | .logo { 22 | flex-shrink: 0; 23 | margin-right: 20px; 24 | } 25 | 26 | .logo img { 27 | height: 40px; /* Adjust size as needed */ 28 | max-width: 160px; /* Adjust the maximum width as needed */ 29 | width: auto; /* Ensure the aspect ratio is maintained */ 30 | max-height: 40px; /* Ensure the logo doesn't exceed the specified height */ 31 | } 32 | 33 | .links { 34 | display: flex; 35 | gap: 20px; 36 | margin-right: 15px; 37 | white-space: nowrap; 38 | } 39 | 40 | .links a { 41 | color: #333333; 42 | text-decoration: none; 43 | font-weight: bold; 44 | flex-shrink: 0; 45 | } 46 | 47 | .links a:hover { 48 | color: #0070f3; /* Example link hover color */ 49 | } 50 | 51 | .logoutButton { 52 | background: none; 53 | border: none; 54 | color: #333333; 55 | cursor: pointer; 56 | text-decoration: none; 57 | padding: 0; 58 | font: inherit; 59 | font-weight: bold; 60 | flex-shrink: 0; 61 | } 62 | 63 | .logoutButton:hover { 64 | color: #0056b3; 65 | } 66 | 67 | /* Adjust styles for mobile or smaller screens */ 68 | @media (max-width: 768px) { 69 | .nav { 70 | flex-direction: row; /* Ensure items stay in a row */ 71 | justify-content: space-between; /* Distribute space evenly */ 72 | align-items: center; /* Center align items vertically */ 73 | } 74 | 75 | .logo { 76 | margin-bottom: 0px; 77 | } 78 | 79 | .links { 80 | gap: 10px; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/components/withAuth.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useSession } from 'next-auth/react'; 4 | import { useRouter } from 'next/navigation'; 5 | import { useEffect } from 'react'; 6 | 7 | const withAuth = (WrappedComponent) => { 8 | return (props) => { 9 | const { data: session, status } = useSession(); 10 | const router = useRouter(); 11 | 12 | useEffect(() => { 13 | if (status === 'unauthenticated') { 14 | router.push('/login'); 15 | } 16 | }, [status]); 17 | 18 | if (status === 'loading') { 19 | return
Loading...
; 20 | } 21 | 22 | if (status === 'authenticated') { 23 | return ; 24 | } 25 | 26 | return null; 27 | }; 28 | }; 29 | 30 | export default withAuth; 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/app/dashboard/api/build/route.js: -------------------------------------------------------------------------------- 1 | import dbConnect from '../../../lib/connectDB'; 2 | import User from '../../../models/User.js'; 3 | import { NextResponse } from 'next/server'; 4 | 5 | export async function GET(request) { 6 | try { 7 | const { searchParams } = new URL(request.url); 8 | const username = searchParams.get('username'); 9 | if (!username) { 10 | return NextResponse.json({ message: 'Username is required' }, { status: 400 }); 11 | } 12 | 13 | const foundUser = await User.findOne({ username }); 14 | 15 | if (!foundUser) { 16 | return NextResponse.json({ message: "User not found in build route" }, { status: 404 }); 17 | } 18 | 19 | // console.log("Found user: ", foundUser); 20 | return NextResponse.json(foundUser.buildTime); 21 | } catch (error) { 22 | // console.error(error); 23 | return NextResponse.json({ error: error.message }, { status: 500 }); 24 | } 25 | } 26 | 27 | export async function POST(request) { 28 | await dbConnect(); 29 | 30 | try { 31 | 32 | const { buildTime } = await request.json(); 33 | const APIkey = request.headers.get('Authorization'); 34 | // console.log('API key:', APIkey); 35 | 36 | if (!APIkey) { 37 | return NextResponse.json({ message: 'API key is required' }, { status: 400 }); 38 | } 39 | 40 | // console.log('going into try block post -- dashb api bundle') 41 | const user = await User.findOne({ APIkey }); 42 | // console.log('User:', user); 43 | if (!user) { 44 | return NextResponse.json({ message: 'API key was not found' }, { status: 409 }); 45 | } 46 | // maybe make new schema with buildDate 47 | const newBuild = { 48 | buildTime, 49 | buildDate: Date.now() 50 | } 51 | user.buildTime.push(newBuild); 52 | 53 | await user.save(); 54 | 55 | return NextResponse.json({ message: 'User build updated successfully'}, { status: 201 }); 56 | } catch (error) { 57 | // console.error(error); 58 | return NextResponse.json({ message: `Internal server error in build post request ${error}` }, { status: 500 }); 59 | } 60 | } -------------------------------------------------------------------------------- /src/app/dashboard/api/bundle/route.js: -------------------------------------------------------------------------------- 1 | import dbConnect from '../../../lib/connectDB.js'; 2 | import User from '../../../models/User.js'; 3 | import { NextResponse } from 'next/server'; 4 | 5 | export async function GET(request) { 6 | try { 7 | const { searchParams } = new URL(request.url); 8 | const username = searchParams.get('username'); 9 | if (!username) { 10 | return NextResponse.json({ message: 'Username is required' }, { status: 400 }); 11 | } 12 | const foundUser = await User.findOne({ username }).lean(); 13 | if (!foundUser) { 14 | return NextResponse.json({ message: "User not found in bundle route" }, { status: 404 }); 15 | } 16 | //console.log("Found user: ", foundUser); 17 | return NextResponse.json(foundUser.bundleLog); 18 | } catch (error) { 19 | // console.error(error); 20 | return NextResponse.json({ error: error.message }, { status: 500 }); 21 | } 22 | } 23 | 24 | export async function POST(request) { 25 | await dbConnect(); 26 | try { 27 | const { bundleLog } = await request.json(); 28 | const APIkey = request.headers.get('Authorization'); 29 | // console.log('API key:', APIkey); 30 | 31 | if (!APIkey) { 32 | return NextResponse.json({ message: 'API key is required' }, { status: 400 }); 33 | } 34 | 35 | // console.log('going into try block post -- dashb api bundle') 36 | const user = await User.findOne({ APIkey }); 37 | // console.log('User:', user); 38 | if (!user) { 39 | return NextResponse.json({ message: 'API key was not found' }, { status: 409 }); 40 | } 41 | 42 | const parsedLog = bundleLog.match(/(Route \(app\)|Page)[\s\S]*/)?.[0] || 'No Bundle Information Found for This Build'; 43 | //match anything starting with Route(app) or Page, keeping everything afterwards including white spaces. 44 | // if no match then display 'No Bundle Information Found for This Build' to render on dashboard 45 | 46 | const newBundle = { 47 | bundleLog: parsedLog, 48 | bundleDate: Date.now() 49 | } 50 | user.bundleLog.push(newBundle); 51 | 52 | await user.save(); 53 | 54 | return NextResponse.json({ message: 'User build updated successfully'}, { status: 201 }); 55 | } catch (error) { 56 | // console.error(error); 57 | return NextResponse.json({ message: `Internal server error in build post request ${error}` }, { status: 500 }); 58 | } 59 | } -------------------------------------------------------------------------------- /src/app/dashboard/api/middleware.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export function middleware(request) { 4 | const response = NextResponse.next(); 5 | response.headers.set('Access-Control-Allow-Credentials', 'true'); 6 | response.headers.set('Access-Control-Allow-Origin', '*'); // Replace '*' with your specific origin if needed 7 | response.headers.set('Access-Control-Allow-Methods', 'GET,DELETE,PATCH,POST,PUT,OPTIONS'); 8 | response.headers.set('Access-Control-Allow-Headers', 'Authorization, Content-Type, Connection, Postman-Token'); 9 | 10 | // Handle OPTIONS method for CORS preflight request 11 | if (request.method === 'OPTIONS') { 12 | return response; 13 | } 14 | 15 | return response; 16 | } 17 | 18 | export const config = { 19 | matcher: '/dashboard/api/:path*', 20 | }; -------------------------------------------------------------------------------- /src/app/dashboard/api/webvitals/route.js: -------------------------------------------------------------------------------- 1 | import dbConnect from '../../../lib/connectDB.js'; 2 | import User from '../../../models/User.js'; 3 | import { NextResponse } from 'next/server'; 4 | 5 | export async function GET(request) { 6 | try { 7 | const { searchParams } = new URL(request.url); 8 | const username = searchParams.get('username'); 9 | const metricType = searchParams.get('metricType'); 10 | const start = searchParams.get('start'); 11 | const end = searchParams.get('end'); 12 | 13 | if (!username || !metricType || !start || !end) { 14 | return NextResponse.json({ message: 'Username, metric type, start date, and end date are required' }, { status: 400 }); 15 | } 16 | const startDate = new Date(start); 17 | const endDate = new Date(end); 18 | 19 | const foundUser = await User.findOne({ username }); 20 | 21 | if (!foundUser) { 22 | return NextResponse.json({ message: "User not found in web vitals route" }, { status: 404 }); 23 | } 24 | const metricData = []; 25 | 26 | foundUser[metricType].forEach(metric => { 27 | const metricDate = new Date(metric["metricDate"]); 28 | if (metricDate >= startDate && metricDate <= endDate) { 29 | metricData.push(metric); 30 | } 31 | }); 32 | 33 | return NextResponse.json(metricData, { status: 200 }); 34 | } catch (error) { 35 | // console.error(error); 36 | return NextResponse.json({ error: error.message }, { status: 500 }); 37 | } 38 | } 39 | 40 | 41 | export async function POST(request) { 42 | await dbConnect(); 43 | try { 44 | const { metricType, metricValue, apiKey } = await request.json(); 45 | // console.log('API key:', apiKey); 46 | 47 | if (!metricType || !metricValue || !apiKey) { 48 | return NextResponse.json({ message: 'Missing infromation in the web vitals post' }, { status: 400 }); 49 | } 50 | 51 | // console.log('going into try web vitals post') 52 | const user = await User.findOne({ APIkey: apiKey }); 53 | // console.log('User:', user); 54 | 55 | if (!user) { 56 | return NextResponse.json({ message: 'API key was not found' }, { status: 409 }); 57 | } 58 | // maybe make new schema with buildDate 59 | const newMetric = { 60 | metricType, 61 | metricValue, 62 | metricDate: Date.now() 63 | } 64 | user[metricType].push(newMetric); 65 | 66 | await user.save(); 67 | 68 | return NextResponse.json({ message: 'Web vitals updated successfully'}, { status: 201 }); 69 | } catch (error) { 70 | // console.error(error); 71 | return NextResponse.json({ message: `Internal server error in web vitals post request ${error}` }, { status: 500 }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/app/dashboard/components/APIkey.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState, useEffect } from 'react'; 4 | import styles from '../dashboard.module.css'; 5 | 6 | function APIKey({ username }) { 7 | //new code 8 | const [ copySuccess, setCopySuccess ] = useState(''); 9 | const [ api, setApi ] = useState(''); 10 | 11 | useEffect(() => { 12 | fetch(`https://www.nextlevel-dash.com/onboarding/api?username=${username}`) 13 | .then((res) => { 14 | if (res.ok) { 15 | // console.log('res:', res); 16 | return res.json(); // not res.json() because its returning 17 | } 18 | }) 19 | .then((data) => { 20 | setApi(data.APIkey); 21 | }) 22 | .catch((error) => { 23 | console.error('Error fetching API key:', error); 24 | }); 25 | }, [username]); 26 | 27 | const copyToClipboard = () => { 28 | navigator.clipboard.writeText(api).then(() => { 29 | setCopySuccess('Copied!'); 30 | setTimeout(() => setCopySuccess(''), 2000); 31 | }, () => { 32 | setCopySuccess('Failed to copy!'); 33 | setTimeout(() => setCopySuccess(''), 2000); 34 | }); 35 | }; 36 | return ( 37 |
38 |

API Key

39 | 45 | 48 | {copySuccess && {copySuccess}} 49 |
50 | ); 51 | } 52 | 53 | export default APIKey; -------------------------------------------------------------------------------- /src/app/dashboard/components/BuildTimeChart.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState, useRef } from 'react'; 4 | import styles from '../dashboard.module.css'; 5 | import { Line } from 'react-chartjs-2'; 6 | import { 7 | Chart as ChartJS, 8 | CategoryScale, 9 | LinearScale, 10 | PointElement, 11 | LineElement, 12 | Title, 13 | Tooltip, 14 | Legend, 15 | TimeScale 16 | } from 'chart.js'; 17 | import 'chartjs-adapter-date-fns'; 18 | import useBuildTimeData from '../hooks/useBuildTimeData'; 19 | 20 | ChartJS.register( 21 | CategoryScale, 22 | LinearScale, 23 | PointElement, 24 | LineElement, 25 | Title, 26 | Tooltip, 27 | Legend, 28 | TimeScale 29 | ); 30 | 31 | 32 | function BuildTimeChart({ username }) { 33 | const chartRef = useRef(null); 34 | const [buildTimeData, setBuildTimeData] = useState([]); 35 | 36 | 37 | useEffect(() => { 38 | useBuildTimeData(username) 39 | .then(data => { 40 | // console.log('Bundle Logs:', logs); 41 | setBuildTimeData(data); 42 | return data; 43 | }).catch(error => { 44 | console.error('Error fetching build time:', error); 45 | }); 46 | }, []); 47 | 48 | // console.log('Build Time Data:', buildTimeData); 49 | 50 | const chartData = { 51 | labels: buildTimeData.map(entry => new Date(entry.buildDate)), 52 | datasets: [ 53 | { 54 | label: 'Build Time', 55 | data: buildTimeData.map(entry => entry.buildTime), 56 | fill: false, 57 | borderColor: 'rgb(75, 192, 192)', 58 | tension: 0.1, 59 | }, 60 | ], 61 | }; 62 | 63 | const options = { 64 | scales: { 65 | x: { 66 | type: 'time', 67 | time: { 68 | unit: 'minute', 69 | tooltipFormat: 'MMM dd, yyyy HH:mm', // Format the tooltip 70 | displayFormats: { 71 | minute: 'MMM dd, yyyy HH:mm', // Format for the x-axis labels 72 | }, 73 | }, 74 | title: { 75 | display: true, 76 | text: 'Date/Time', 77 | }, 78 | }, 79 | y: { 80 | title: { 81 | display: true, 82 | text: 'Speed (ms)', 83 | }, 84 | }, 85 | }, 86 | responsive: true, 87 | plugins: { 88 | legend: { 89 | position: 'top', 90 | }, 91 | title: { 92 | display: true, 93 | }, 94 | }, 95 | }; 96 | 97 | const downloadChart = () => { 98 | const chartInstance = chartRef.current; 99 | if (chartInstance) { 100 | const link = document.createElement('a'); 101 | link.href = chartInstance.toBase64Image(); 102 | link.download = 'build-time-chart.png'; 103 | link.click(); 104 | } 105 | }; 106 | 107 | return ( 108 |
109 |
110 | 111 |
112 | 113 |
114 | ); 115 | } 116 | 117 | export default BuildTimeChart; 118 | -------------------------------------------------------------------------------- /src/app/dashboard/components/BuildTimeContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from '../dashboard.module.css'; 3 | import BuildTimeChart from './BuildTimeChart'; 4 | import BundleLog from './BundleLog'; 5 | 6 | function BuildTimeContainer({ username }) { 7 | return ( 8 |
9 |

Build Time

10 |
11 | 12 | 13 |
14 |
15 | ); 16 | } 17 | 18 | export default BuildTimeContainer; 19 | -------------------------------------------------------------------------------- /src/app/dashboard/components/BundleLog.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import styles from '../dashboard.module.css'; 3 | import useBundleData from '../hooks/useBundleData'; 4 | import { set } from 'mongoose'; 5 | 6 | 7 | function BundleLog({username}) { 8 | console.log('entering build time metrics for username:', username); 9 | 10 | const [bundleLogs, setBundleLogs] = useState([]); 11 | const [currentIndex, setCurrentIndex] = useState(0); 12 | const [currentLog, setCurrentLog] = useState({}); 13 | 14 | 15 | useEffect(() => { 16 | useBundleData(username) 17 | .then(logs => { 18 | // console.log('Bundle Logs:', logs); 19 | setBundleLogs(logs); 20 | return logs; 21 | }).then(logs => { 22 | // console.log('logs length ', logs.length) 23 | const logsLength = logs.length 24 | setCurrentIndex(logsLength - 1); 25 | // console.log('Current Index:', currentIndex); 26 | setCurrentLog(bundleLogs[currentIndex]); 27 | // console.log('Current Log:', currentLog); 28 | }) 29 | .catch(error => { 30 | console.error('Error fetching bundle log:', error); 31 | }); 32 | 33 | }, []); 34 | 35 | useEffect(() => { 36 | const logsLength = bundleLogs.length; 37 | setCurrentIndex(logsLength - 1); 38 | // console.log('Current Index:', currentIndex); 39 | setCurrentLog(bundleLogs[currentIndex]); 40 | // console.log('Current Log:', currentLog); 41 | }, [bundleLogs]); 42 | 43 | const toggleBack = () => { 44 | if (currentIndex > 0) { 45 | setCurrentIndex(currentIndex - 1); 46 | setCurrentLog(bundleLogs[currentIndex - 1]); 47 | } 48 | } 49 | 50 | const toggleForward = () => { 51 | if (currentIndex < bundleLogs.length - 1) { 52 | setCurrentIndex(currentIndex + 1); 53 | setCurrentLog(bundleLogs[currentIndex + 1]); 54 | } 55 | } 56 | 57 | const formatDateTime = (dateTime) => { 58 | const date = new Date(dateTime); 59 | return date.toLocaleString(); 60 | }; 61 | 62 | if (!currentLog) { 63 | return
No logs available.
; // or handle accordingly 64 | } 65 | 66 | 67 | return ( 68 |
69 |

Bundle Logs

70 |
71 |
72 | 73 | {/* back doesn't work when index = 0, next doesnt work when index = length-1 */} 74 |
{formatDateTime(currentLog.bundleDate)}
75 | 76 |
77 |
78 |
{currentLog.bundleLog}
79 |
80 | 81 |
82 |
83 | ); 84 | } 85 | 86 | export default BundleLog; -------------------------------------------------------------------------------- /src/app/dashboard/components/CLSChart.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState, useRef } from 'react'; 4 | import styles from '../dashboard.module.css'; 5 | import { Line } from 'react-chartjs-2'; 6 | import { 7 | Chart as ChartJS, 8 | CategoryScale, 9 | LinearScale, 10 | PointElement, 11 | LineElement, 12 | Title, 13 | Tooltip, 14 | Legend, 15 | TimeScale 16 | } from 'chart.js'; 17 | import 'chartjs-adapter-date-fns'; 18 | import useWebVitalsData from '../hooks/useWebVitalsData'; 19 | import WebVitalsFilter from './WebVitalsFilter'; 20 | 21 | ChartJS.register( 22 | CategoryScale, 23 | LinearScale, 24 | PointElement, 25 | LineElement, 26 | Title, 27 | Tooltip, 28 | Legend, 29 | TimeScale 30 | ); 31 | 32 | function CLSChart({ clsData }) { 33 | const chartRef = useRef(null); 34 | 35 | const chartData = { 36 | labels: clsData.map(entry => new Date(entry.metricDate)), 37 | datasets: [ 38 | { 39 | label: 'Cumulative Layout Shift', 40 | data: clsData.map(entry => entry.metricValue), 41 | fill: false, 42 | borderColor: 'rgb(255,189,89)', 43 | tension: 0.1, 44 | }, 45 | ], 46 | }; 47 | 48 | const options = { 49 | scales: { 50 | x: { 51 | type: 'time', 52 | time: { 53 | unit: 'minute', 54 | tooltipFormat: 'MMM dd, yyyy HH:mm', 55 | displayFormats: { 56 | minute: 'MMM dd, yyyy HH:mm', 57 | }, 58 | }, 59 | title: { 60 | display: true, 61 | text: 'Date/Time', 62 | }, 63 | }, 64 | y: { 65 | title: { 66 | display: true, 67 | text: 'CLS Value', 68 | }, 69 | }, 70 | }, 71 | responsive: true, 72 | plugins: { 73 | legend: { 74 | position: 'top', 75 | }, 76 | title: { 77 | display: true, 78 | }, 79 | }, 80 | }; 81 | 82 | const downloadChart = () => { 83 | const chartInstance = chartRef.current; 84 | if (chartInstance) { 85 | const link = document.createElement('a'); 86 | link.href = chartInstance.toBase64Image(); 87 | link.download = 'cls-chart.png'; 88 | link.click(); 89 | } 90 | }; 91 | 92 | return ( 93 |
94 |
95 | 96 |
97 | 98 |
99 | ); 100 | } 101 | 102 | export default CLSChart; -------------------------------------------------------------------------------- /src/app/dashboard/components/Rating.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import styles from '../dashboard.module.css'; 3 | 4 | const Rating = ({ goodRange, needsImprovementRange, poorRange, metricType, currentValue }) => { 5 | const chartRef = useRef(null); 6 | 7 | useEffect(() => { 8 | const createChart = () => { 9 | const chart = JSC.chart(chartRef.current, { 10 | debug: true, 11 | type: 'gauge', 12 | legend_visible: false, 13 | chartArea_boxVisible: true, 14 | width: 175, 15 | height: 150, 16 | defaultFont: 'Poppins, sans-serif', 17 | xAxis: { 18 | scale: { range: [0, 1], invert: true } 19 | }, 20 | palette: { 21 | pointValue: '%yValue', 22 | ranges: [ 23 | { value: [goodRange[0], goodRange[1]], color: '#008000' }, 24 | { value: [needsImprovementRange[0], needsImprovementRange[1]], color: '#FFD221' }, 25 | { value: [poorRange[0], poorRange[1]], color: '#FF5353' } 26 | ] 27 | }, 28 | yAxis: { 29 | defaultTick: { padding: 3, enabled: false }, 30 | customTicks: [goodRange[0], goodRange[1], needsImprovementRange[1], poorRange[1]], 31 | // customTicks: [goodRange[0], poorRange[1]], // Updated customTicks to reflect correct ranges 32 | line: { 33 | width: 15, 34 | breaks_gap: 0.05, 35 | color: 'smartPalette' 36 | }, 37 | scale: { range: [goodRange[0], poorRange[1]] } 38 | }, 39 | defaultSeries: { 40 | opacity: 1, 41 | shape: { 42 | label: { 43 | align: 'center', 44 | verticalAlign: 'middle' 45 | } 46 | } 47 | }, 48 | series: [ 49 | { 50 | type: 'marker', 51 | name: 'Score', 52 | shape_label: { 53 | text: `${currentValue ? `${metricType !== 'CLS' ? `${currentValue} ms` : currentValue}` : ''}
${currentValue ? (currentValue <= goodRange[1] ? 'Great!' : currentValue <= needsImprovementRange[1] ? 'Needs Work' : 'Poor') : `No Data`}`, 54 | style: { fontSize: 20, fontFamily: 'Poppins, sans-serif' } 55 | }, 56 | defaultPoint: { 57 | tooltip: '%yValue', 58 | marker: { 59 | outline: { 60 | width: 10, 61 | color: 'currentColor' 62 | }, 63 | fill: 'white', 64 | type: 'circle', 65 | visible: true, 66 | size: 30 67 | } 68 | }, 69 | points: [[1, currentValue]] 70 | } 71 | ] 72 | }); 73 | }; 74 | 75 | createChart(); 76 | }, [poorRange, needsImprovementRange, goodRange, currentValue]); 77 | 78 | return ( 79 |
80 |

{metricType}

81 |
82 |
83 | ); 84 | }; 85 | 86 | export default Rating; -------------------------------------------------------------------------------- /src/app/dashboard/components/Sidebar.js: -------------------------------------------------------------------------------- 1 | import styles from '../dashboard.module.css'; 2 | import { IoSettingsOutline } from "react-icons/io5"; 3 | import Link from 'next/link'; 4 | 5 | function SideBar(props) { 6 | // console.log('Props sidebar:', props); 7 | const username = props.username; 8 | // console.log('Username sidebar:', username); 9 | return ( 10 |
11 |
12 | {/*
    13 | 14 |
  • Onboarding
  • 15 | 16 | 17 |
  • History
  • 18 | 19 |
*/} 20 |
21 |
22 | 23 |
    24 | 25 |
  • 26 |
  • Set Up
  • 27 |
28 | 29 |
30 |
31 | ); 32 | } 33 | 34 | export default SideBar; 35 | -------------------------------------------------------------------------------- /src/app/dashboard/components/WebVitalRatings.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import Rating from './Rating'; 3 | import styles from '../dashboard.module.css'; 4 | 5 | const WebVitalRatings = ({ data }) => { 6 | const ranges = { 7 | //we set the upper limits, may need to adjust 8 | "TTFB": [0, 800, 1800, 3000], 9 | "LCP": [0, 2500, 4000, 7000], 10 | "FCP": [0, 1800, 3000, 5000], 11 | "FID": [0, 100, 300, 450], 12 | "INP": [0, 200, 500, 800], 13 | "CLS": [0, 0.1, 0.25, .75], 14 | } 15 | const averages = { 16 | "TTFB": [0, 0], 17 | "LCP": [0, 0], 18 | "FCP": [0, 0], 19 | "FID": [0, 0], 20 | "INP": [0, 0], 21 | "CLS": [0, 0], 22 | } 23 | const [vitalRatings, setVitalRatings] = useState([]); 24 | useEffect(() => { 25 | data.forEach(entry => { 26 | switch (entry.metricType) { 27 | case "TTFB": 28 | averages["TTFB"][0] += entry.metricValue; 29 | averages["TTFB"][1]++; 30 | break; 31 | case "LCP": 32 | averages["LCP"][0] += entry.metricValue; 33 | averages["LCP"][1]++; 34 | break; 35 | case "FCP": 36 | averages["FCP"][0] += entry.metricValue; 37 | averages["FCP"][1]++; 38 | break; 39 | case "FID": 40 | averages["FID"][0] += entry.metricValue; 41 | averages["FID"][1]++; 42 | break; 43 | case "INP": 44 | averages["INP"][0] += entry.metricValue; 45 | averages["INP"][1]++; 46 | break; 47 | case "CLS": 48 | averages["CLS"][0] += entry.metricValue; 49 | averages["CLS"][1]++; 50 | break; 51 | default: 52 | break; 53 | } 54 | }); 55 | 56 | const metrics = Object.keys(ranges); 57 | const vitals = metrics.map(metric => { 58 | const unrounded = averages[metric][0] / averages[metric][1]; 59 | let val; 60 | if(metric === "CLS") { 61 | if(!unrounded) { 62 | val = Math.round(unrounded); 63 | } else { 64 | val = unrounded.toFixed(4); 65 | } 66 | } else { 67 | val = Math.round(unrounded); 68 | } 69 | return ( 70 | 77 | ); 78 | }); 79 | setVitalRatings(vitals); 80 | }, [data]); 81 | 82 | return ( 83 |
84 |
85 | {vitalRatings} 86 |
87 |
88 | ); 89 | }; 90 | 91 | export default WebVitalRatings; -------------------------------------------------------------------------------- /src/app/dashboard/components/WebVitalsChart.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState, useRef } from 'react'; 4 | import styles from '../dashboard.module.css'; 5 | import { Line } from 'react-chartjs-2'; 6 | import { 7 | Chart as ChartJS, 8 | CategoryScale, 9 | LinearScale, 10 | PointElement, 11 | LineElement, 12 | Title, 13 | Tooltip, 14 | Legend, 15 | TimeScale 16 | } from 'chart.js'; 17 | import 'chartjs-adapter-date-fns'; 18 | import useWebVitalsData from '../hooks/useWebVitalsData'; 19 | 20 | ChartJS.register( 21 | CategoryScale, 22 | LinearScale, 23 | PointElement, 24 | LineElement, 25 | Title, 26 | Tooltip, 27 | Legend, 28 | TimeScale 29 | ); 30 | 31 | function WebVitalsChart({ webVitalsData }) { 32 | const chartRef = useRef(null); 33 | const chartData = { 34 | labels: webVitalsData.map(entry => new Date(entry.metricDate)), 35 | datasets: [ 36 | { 37 | label: 'Time to First Byte (ms)', 38 | data: webVitalsData.map(entry => { 39 | if (entry.metricType === 'TTFB') { 40 | return entry.metricValue; 41 | } else { 42 | return null; 43 | } 44 | }), 45 | fill: false, 46 | borderColor: 'rgb(75, 192, 192)', 47 | tension: 0.1, 48 | }, 49 | { 50 | label: 'Largest Contentful Paint (ms)', 51 | data: webVitalsData.map(entry => { 52 | if (entry.metricType === 'LCP') { 53 | return entry.metricValue; 54 | } else { 55 | return null; 56 | } 57 | }), 58 | fill: false, 59 | borderColor: 'rgb(199,235,185)', 60 | tension: 0.1, 61 | }, 62 | { 63 | label: 'First Contentful Paint (ms)', 64 | data: webVitalsData.map(entry => { 65 | if (entry.metricType === 'FCP') { 66 | return entry.metricValue; 67 | } else { 68 | return null; 69 | } 70 | }), 71 | fill: false, 72 | borderColor: 'rgb(255, 99, 132)', 73 | tension: 0.1, 74 | }, 75 | { 76 | label: 'First Input Delay (ms)', 77 | data: webVitalsData.map(entry => { 78 | if (entry.metricType === 'FID') { 79 | return entry.metricValue; 80 | } else { 81 | return null; 82 | } 83 | }), 84 | fill: false, 85 | borderColor: 'rgb(153, 102, 255)', 86 | tension: 0.1, 87 | }, 88 | { 89 | label: 'Interaction to Next Paint (ms)', 90 | data: webVitalsData.map(entry => { 91 | if (entry.metricType === 'INP') { 92 | return entry.metricValue; 93 | } else { 94 | return null; 95 | } 96 | }), 97 | fill: false, 98 | borderColor: 'rgb(54, 162, 235)', 99 | tension: 0.1, 100 | }, 101 | ], 102 | }; 103 | 104 | const options = { 105 | scales: { 106 | x: { 107 | type: 'time', 108 | time: { 109 | unit: 'minute', 110 | tooltipFormat: 'MMM dd, yyyy HH:mm', // Format the tooltip 111 | displayFormats: { 112 | minute: 'MMM dd, yyyy HH:mm', // Format for the x-axis labels 113 | }, 114 | }, 115 | title: { 116 | display: true, 117 | text: 'Date/Time', 118 | }, 119 | }, 120 | y: { 121 | title: { 122 | display: true, 123 | text: 'Speed (ms)', 124 | }, 125 | }, 126 | }, 127 | responsive: true, 128 | plugins: { 129 | legend: { 130 | position: 'top', 131 | }, 132 | title: { 133 | display: true, 134 | }, 135 | }, 136 | }; 137 | 138 | const downloadChart = () => { 139 | const chartInstance = chartRef.current; 140 | if (chartInstance) { 141 | const link = document.createElement('a'); 142 | link.href = chartInstance.toBase64Image(); 143 | link.download = 'web-vitals-chart.png'; 144 | link.click(); 145 | } 146 | }; 147 | 148 | return ( 149 |
150 |
151 | 152 |
153 | 156 |
157 | ); 158 | } 159 | 160 | export default WebVitalsChart; 161 | -------------------------------------------------------------------------------- /src/app/dashboard/components/WebVitalsContainer.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState, useRef } from 'react'; 4 | import styles from '../dashboard.module.css'; 5 | import useWebVitalsData from '../hooks/useWebVitalsData'; 6 | import WebVitalsFilter from './WebVitalsFilter'; 7 | import CLSChart from './CLSChart'; 8 | import WebVitalRatings from './WebVitalRatings'; 9 | import WebVitalsChart from './WebVitalsChart'; 10 | 11 | function WebVitalsContainer({ username}) { 12 | const [webVitalsData, setWebVitalsData] = useState([]); 13 | const [clsData, setClsData] = useState([]); 14 | const chartRef = useRef(null); 15 | // set end date to one day ago 16 | // set start date to now 17 | const now = new Date(); 18 | const defaultEnd = now.toISOString().slice(0, 16); 19 | const oneDayAgo = new Date(now.getTime() - (24 * 60 * 60 * 1000)); 20 | const defaultStart = oneDayAgo.toISOString().slice(0, 16); // format to 'YYYY-MM-DDTHH:MM' 21 | 22 | const [startDate, setStartDate] = useState(defaultStart); 23 | const [endDate, setEndDate] = useState(defaultEnd); 24 | 25 | const onSubmit = (startDate, endDate) => { 26 | setStartDate(startDate); 27 | setEndDate(endDate); 28 | }; 29 | 30 | useEffect(() => { 31 | useWebVitalsData(username, startDate, endDate) 32 | .then(data => { 33 | setWebVitalsData(data); 34 | const filteredData = data.filter(entry => entry.metricType === 'CLS'); 35 | setClsData(filteredData); 36 | return data; 37 | }).catch(error => { 38 | console.error('Error fetching web vitals', error); 39 | }); 40 | }, [username, startDate, endDate]); 41 | 42 | // console.log('Web Vitals Data:', webVitalsData); 43 | 44 | return ( 45 |
46 |
47 |

Web Vitals

48 | 49 |
50 |
51 | 52 |
53 |
54 | 55 | 56 |
57 |
58 | ); 59 | } 60 | 61 | export default WebVitalsContainer; 62 | 63 | -------------------------------------------------------------------------------- /src/app/dashboard/components/WebVitalsFilter.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState } from 'react'; 4 | import styles from '../dashboard.module.css'; 5 | 6 | const WebVitalsFilter = ({ onSubmit }) => { 7 | const [startDate, setStartDate] = useState(''); 8 | const [endDate, setEndDate] = useState(''); 9 | 10 | const handleSubmit = (event) => { 11 | event.preventDefault(); 12 | // call the onUpdate function passed as a prop with the new start and end dates 13 | onSubmit(startDate, endDate); 14 | }; 15 | 16 | const handleStartDateChange = (event) => { 17 | setStartDate(event.target.value); 18 | }; 19 | 20 | const handleEndDateChange = (event) => { 21 | setEndDate(event.target.value); 22 | }; 23 | 24 | return ( 25 |
26 |
27 | 28 | 29 | 30 |
31 |
32 | ); 33 | } 34 | 35 | export default WebVitalsFilter; 36 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.module.css: -------------------------------------------------------------------------------- 1 | .dashboardContainer { 2 | display: flex; 3 | height: 100vh; 4 | /* overflow: hidden; */ 5 | background-color: #e7e7e7; 6 | } 7 | 8 | .sidebar { 9 | position: fixed; 10 | top: 70px; /* Adjust this value to match the height of your top nav */ 11 | left: 0; 12 | bottom: 0; 13 | width: 15%; 14 | background-color: #143A52; 15 | padding: 20px; 16 | box-sizing: border-box; 17 | display: flex; 18 | flex-direction: column; 19 | justify-content: space-between; 20 | overflow: hidden; 21 | text-decoration: none; 22 | } 23 | 24 | .top-section{ 25 | padding: 20px; 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: flex-start; 29 | box-sizing: border-box; 30 | margin-left: -10px; 31 | /* border: 3px solid yellow; */ 32 | } 33 | 34 | .sidebarTop{ 35 | margin-left: -20px; 36 | text-decoration: none; 37 | } 38 | 39 | .bottom-section { 40 | padding: 20px; 41 | display: flex; 42 | flex-direction: column; 43 | justify-content: flex-end; 44 | box-sizing: border-box; 45 | /* border: 3px solid red; */ 46 | } 47 | 48 | .sidebarBottom{ 49 | display: flex; 50 | flex-direction: row; 51 | margin-left: -20px; 52 | } 53 | 54 | .sidebarLink { 55 | display: flex; 56 | align-items: center; 57 | text-decoration: none; 58 | color: white; 59 | padding: 10px 0; 60 | overflow: hidden; /* Prevent content from bleeding out */ 61 | white-space: nowrap; /* Prevent text from wrapping */ 62 | text-overflow: ellipsis; /* Show ellipsis for overflow text */ 63 | } 64 | 65 | .link { 66 | color: #e7e7e7; 67 | text-decoration: none; 68 | overflow: hidden; /* Prevent content from bleeding out */ 69 | white-space: nowrap; /* Prevent text from wrapping */ 70 | text-overflow: ellipsis; /* Show ellipsis for overflow text */ 71 | } 72 | 73 | .sidebarListItems { 74 | color: #e7e7e7; 75 | display: block; 76 | font-size: 20px; 77 | text-decoration: none; 78 | overflow: hidden; /* Prevent content from bleeding out */ 79 | white-space: nowrap; /* Prevent text from wrapping */ 80 | text-overflow: ellipsis; /* Show ellipsis for overflow text */ 81 | } 82 | 83 | .sidebarListIcons { 84 | color: #e7e7e7; 85 | margin-right: 5px; 86 | display: inline-block; 87 | vertical-align: middle; 88 | margin-top: 2px; 89 | font-size: 25px; 90 | overflow: hidden; /* Prevent content from bleeding out */ 91 | white-space: nowrap; /* Prevent text from wrapping */ 92 | text-overflow: ellipsis; /* Show ellipsis for overflow text */ 93 | } 94 | 95 | .mainContent { 96 | margin-left: 15%; /* Add this to account for the fixed sidebar */ 97 | padding-top: 60px; /* Add padding to account for the top nav */ 98 | width: 85%; 99 | padding: 20px; 100 | box-sizing: border-box; 101 | display: flex; 102 | flex-direction: column; 103 | gap: 20px; 104 | overflow-y: auto; /* Allow scrolling for the main content */ 105 | /* height: calc(100vh - 60px); Subtract the height of the top nav */ 106 | } 107 | 108 | .apiContainer { 109 | display: flex; 110 | align-items: center; 111 | align-self: center; 112 | gap: 10px; 113 | padding-top: 80px; 114 | width: 97.5%; 115 | } 116 | 117 | #apiTitle { 118 | margin-right: 10px; 119 | } 120 | 121 | .apiCodeInput { 122 | flex: 1; 123 | padding: 10px; 124 | border: 1px solid #ddd; 125 | border-radius: 4px; 126 | font-size: 14px; 127 | } 128 | 129 | .copyButton { 130 | padding: 10px 20px; 131 | background-color: #007bff; 132 | color: white; 133 | border: none; 134 | border-radius: 4px; 135 | cursor: pointer; 136 | font-size: 14px; 137 | font-family: 'Poppins', sans-serif; 138 | } 139 | 140 | .copyButton:hover { 141 | background-color: #0056b3; 142 | transform: translateY(2px); 143 | } 144 | 145 | .copySuccess { 146 | margin-left: 10px; 147 | color: green; 148 | font-size: 14px; 149 | } 150 | 151 | .chartContainer { 152 | position: relative; 153 | background-color: #ffffff; 154 | border: 1px solid #ddd; 155 | border-radius: 8px; 156 | padding: 20px; 157 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 158 | display: flex; 159 | flex-direction: column; 160 | gap: 20px; 161 | margin: 10px; 162 | justify-content: flex-start; 163 | align-items: stretch; 164 | width: 98%; 165 | height: auto; 166 | box-sizing: border-box; 167 | } 168 | 169 | .chartHeader { 170 | display: flex; 171 | justify-content: space-between; 172 | align-items: center; 173 | padding: 0 0 10px 0; 174 | 175 | } 176 | 177 | .chartTitle { 178 | margin: 0 0 5px 0; /* Add bottom margin to space title from the chart */ 179 | flex-shrink: 0; /* Prevent title from shrinking */ 180 | } 181 | 182 | .webVitalsChartContainer { 183 | display: flex; 184 | align-items: center; 185 | align-self: center; 186 | justify-content: center; 187 | 188 | } 189 | 190 | .webVitalsChart { 191 | display: flex; 192 | align-items: center; 193 | align-self: center; 194 | justify-content: center; 195 | width: 80%; 196 | min-width: 300px; 197 | box-sizing: border-box; 198 | } 199 | 200 | 201 | .buildCharts { 202 | display: flex; 203 | gap: 20px; 204 | flex-wrap: nowrap; 205 | width: 100%; 206 | height: auto; /* Allow the container to adjust its height based on its content */ 207 | align-items: stretch; /* Ensures all children stretch to fill the container vertically */ 208 | overflow: hidden; /* Prevents children from overflowing */ 209 | } 210 | 211 | .buildTimeDiv { 212 | width: 50%; 213 | height: auto; 214 | } 215 | .buildTimeChart { 216 | display: flex; 217 | align-items: center; 218 | justify-content: center; 219 | height: 85%; 220 | width: 100%; 221 | min-width: 300px; /* Minimum width to prevent them from getting too small */ 222 | box-sizing: border-box; 223 | overflow: hidden; /* Additional overflow handling within the chart area */ 224 | } 225 | 226 | .bundleLogContainer { 227 | flex: 1; 228 | display: flex; 229 | flex-direction: column; 230 | align-items: stretch; 231 | height: 100%; 232 | width: 100%; 233 | min-width: 300px; /* Minimum width to prevent them from getting too small */ 234 | box-sizing: border-box; 235 | overflow: hidden; /* Prevent content from bleeding out */ 236 | } 237 | 238 | .bundleLogTitle { 239 | align-self: center; 240 | } 241 | .bundleToggleBox { 242 | background-color: #ffffff; 243 | width: 100%; 244 | } 245 | 246 | .bundleHeader { 247 | display: flex; 248 | justify-content: space-between; 249 | align-items: center; 250 | background-color: #c9c9c9; 251 | padding: 0.5em 1em; 252 | font-size: 0.9em; 253 | border-bottom: 1px solid #eaeaea; 254 | border-radius: 10px 10px 0px 0px; 255 | box-sizing: border-box; 256 | } 257 | 258 | .bundleLogTitle { 259 | margin: 2px; 260 | } 261 | 262 | .bundleLogContent { 263 | padding: 1em; 264 | background-color: #ffffff; 265 | border-radius: 0px 0px 10px 10px; 266 | flex: 1; /* Allow the chart to grow and take up remaining space */ 267 | overflow: auto; /* Add overflow auto to handle any content overflow */ 268 | box-sizing: border-box; 269 | } 270 | 271 | .toggleButton { 272 | font-size: 1.1rem; 273 | background-color: #c9c9c9; 274 | border: none; 275 | color: #143A52; 276 | } 277 | 278 | .toggleButton:disabled { 279 | background-color: #c9c9c9; 280 | color: #a1a1a1; 281 | cursor: not-allowed; 282 | } 283 | 284 | .bundleLog { 285 | display: flex; 286 | padding: 0 15px; 287 | background-color: #e9e9e9; 288 | overflow-x: auto; 289 | border-radius: 0px 0px 10px 10px ; 290 | } 291 | 292 | .downloadButton { 293 | padding: 5px 10px; 294 | margin: 3px; 295 | margin-top: 25px; 296 | background-color: #ffffff; /* Set background to white */ 297 | color: #7e7e7e; /* Set text color to grey */ 298 | border: 1px solid #7e7e7e; /* Set border color to grey */ 299 | border-radius: 5px; 300 | cursor: pointer; 301 | width: 82px; 302 | display: flex; 303 | justify-content: flex-start; 304 | font-family: 'Poppins', sans-serif; 305 | } 306 | 307 | .downloadButton:hover { 308 | background-color: #007bff; /* Set background color to blue on hover */ 309 | color: white; /* Set text color to white on hover */ 310 | border: 1px solid #007bff; /* Set border color to blue on hover */ 311 | transform: translateY(2px); 312 | } 313 | 314 | .webVitalsFilter { 315 | display: flex; 316 | flex-direction: row; 317 | } 318 | 319 | .dateFilter { 320 | display: flex; 321 | justify-content: space-between; 322 | align-items: center; 323 | margin: 0 5px; 324 | padding: 0 10px; 325 | } 326 | 327 | .filterDateSubmit { 328 | padding: 5px 10px; 329 | margin-left: 10px; 330 | background-color: #007bff; 331 | color: white; 332 | border: none; 333 | border-radius: 5px; 334 | cursor: pointer; 335 | font-family: 'Poppins', sans-serif; 336 | } 337 | 338 | .filterDateSubmit:hover { 339 | background-color: #4b8acf; 340 | transform: translateY(2px); 341 | } 342 | 343 | .clsAndRatings { 344 | display: flex; 345 | flex-direction: row; 346 | width: 100%; 347 | } 348 | 349 | .clsContainer { 350 | display: flex; 351 | flex-direction: column; 352 | flex:1; 353 | } 354 | 355 | .ratingsContainerDiv { 356 | display: flex; 357 | flex-direction: column; 358 | width: max-content; 359 | } 360 | 361 | .ratingsContainer { 362 | display: grid; 363 | grid-template-columns: repeat(2, 1fr); 364 | width: 350px; 365 | margin-left: 10px; 366 | } 367 | 368 | .ratingGauge { 369 | width: 175px; 370 | height: 150px; 371 | } 372 | 373 | .ratingDiv { 374 | display: flex; 375 | flex-direction: column; 376 | justify-content: center; 377 | } 378 | 379 | .ratingHeading { 380 | align-self: center; 381 | margin: 0; 382 | padding: 0; 383 | font-size: 15px; 384 | } -------------------------------------------------------------------------------- /src/app/dashboard/hooks/useBuildTimeData.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState } from 'react'; 4 | 5 | const useBuildTimeData = async (username) => { 6 | // console.log('entering use effect useBuildTimeData for:', username); 7 | try { 8 | const res = await fetch(`https://www.nextlevel-dash.com/dashboard/api/build?username=${username}`); 9 | // const res = await fetch(`http://localhost:3000/dashboard/api/build?username=${username}`); 10 | if (res.ok) { 11 | // console.log('Res from useBuildTimeData:', res); 12 | const data = await res.json(); 13 | // console.log('Data from useBuildTimeData:', data); 14 | return data; 15 | } else { 16 | console.error('Response not ok'); 17 | return null; 18 | } 19 | } catch (error) { 20 | console.error('Error fetching bundle log:', error); 21 | return null; 22 | } 23 | }; 24 | 25 | export default useBuildTimeData; -------------------------------------------------------------------------------- /src/app/dashboard/hooks/useBundleData.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState } from 'react'; 4 | 5 | const useBundleData = async (username) => { 6 | // console.log('entering use effect usebundledata for:', username); 7 | try { 8 | const res = await fetch(`https://www.nextlevel-dash.com/dashboard/api/bundle?username=${username}`); 9 | // const res = await fetch(`http://localhost:3000/dashboard/api/bundle?username=${username}`); 10 | if (res.ok) { 11 | // console.log('res from useBundleData:', res); 12 | const data = await res.json(); 13 | // console.log('Data from useBundleData:', data); 14 | return data; 15 | } else { 16 | console.error('Response not ok'); 17 | return null; 18 | } 19 | } catch (error) { 20 | console.error('Error fetching bundle log:', error); 21 | return null; 22 | } 23 | }; 24 | 25 | export default useBundleData; -------------------------------------------------------------------------------- /src/app/dashboard/hooks/useWebVitalsData.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | async function fetchWebVitals (username, metricType, startDate, endDate) { 4 | try { 5 | const res = await fetch(`https://www.nextlevel-dash.com/dashboard/api/webvitals?username=${username}&metricType=${metricType}&start=${startDate}&end=${endDate}`); 6 | // const res = await fetch(`http://localhost:3000/dashboard/api/webvitals?username=${username}&metricType=${metricType}&start=${startDate}&end=${endDate}`); 7 | if (res.ok) { 8 | // console.log('Res from useWebVitalsData:', res); 9 | const resData = await res.json(); 10 | // console.log('Data from useWebVitalsData:', resData); 11 | return resData; //array of obj with all metric types 12 | } else { 13 | console.error('Response not ok'); 14 | return []; 15 | } 16 | } catch (error) { 17 | console.error('Error fetching web vitals', error); 18 | return []; 19 | } 20 | } 21 | 22 | const useWebVitalsData = async (username, startDate, endDate) => { 23 | // console.log('entering use effect useWebVitalsData for:', username); 24 | const metricTypes = ['FCP', 'LCP', 'TTFB', 'FID', 'INP', 'CLS']; 25 | try { 26 | const metricsCombined = await Promise.all(metricTypes.map(metricType => fetchWebVitals(username, metricType, startDate, endDate))); 27 | // console.log('Metrics Combined:', metricsCombined.flat()); 28 | return metricsCombined.flat(); // Flatten the array of arrays into a single array 29 | } catch (error) { 30 | console.error('Error in useWebVitals:', error); 31 | } 32 | }; 33 | 34 | export default useWebVitalsData; -------------------------------------------------------------------------------- /src/app/dashboard/layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './dashboard.module.css'; 3 | 4 | 5 | export default function DashboardLayout({ children }) { 6 | return ( 7 |
8 | {children} 9 |
10 | ); 11 | } -------------------------------------------------------------------------------- /src/app/dashboard/page.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState } from 'react'; 4 | import styles from './dashboard.module.css'; 5 | import WebVitalsContainer from './components/WebVitalsContainer'; 6 | import APIKey from './components/APIkey'; 7 | import BuildTimeContainer from './components/BuildTimeContainer'; 8 | import withAuth from '../components/withAuth'; 9 | import SideBar from './components/Sidebar'; 10 | 11 | function Dashboard(props) { 12 | const [username, setUsername] = useState(''); 13 | 14 | useEffect(() => { 15 | const currentUrl = window.location.href; 16 | console.log('Current URL:', currentUrl); 17 | const url = new URL(currentUrl); 18 | const usernameFromUrl = url.searchParams.get('username'); 19 | console.log('Username:', usernameFromUrl); 20 | setUsername(usernameFromUrl); 21 | }, []); 22 | 23 | if (!username) { 24 | return null; 25 | } 26 | return ( 27 |
28 | 29 |
30 | 31 | 32 | 33 |
34 |
35 | ); 36 | } 37 | 38 | export default withAuth(Dashboard); 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/app/dashboard/settings/page.js: -------------------------------------------------------------------------------- 1 | //not currently in use 2 | 3 | import React from 'react' 4 | 5 | const Settings = () => { 6 | return ( 7 |
8 |

Settings

9 |
10 | ) 11 | } 12 | 13 | export default Settings -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | /* global.css */ 2 | body, html { 3 | margin: 0; 4 | padding: 0; 5 | font-family: 'Poppins', sans-serif; 6 | height: 100%; 7 | background-color: #1b212c; 8 | /* display: flex; 9 | justify-content: center; 10 | align-items: center; */ 11 | } 12 | 13 | * { 14 | box-sizing: border-box; 15 | } 16 | 17 | body.loading { 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | min-height: 100vh; 22 | overflow: hidden; 23 | } 24 | 25 | body.loaded { 26 | display: block; 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/app/home.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@700&display=swap'); 2 | 3 | :global .your-page-class * { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | .navbar-gap { 9 | width: 100%; 10 | height: 50px; 11 | } 12 | 13 | section { 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | flex-direction: column; 18 | min-height: 80vh; 19 | background: #1B212C; 20 | color: #ffffff; 21 | box-sizing: border-box; 22 | font-family: 'Poppins', sans-serif; 23 | overflow-x: hidden; 24 | } 25 | 26 | h1 { 27 | font-size: 70px; 28 | color: #fff; 29 | margin: 0; 30 | } 31 | 32 | p { 33 | font-size: 35px; 34 | color: #489ae7; 35 | font-weight: 600; 36 | } 37 | 38 | section .animate { 39 | opacity: 0; 40 | filter: blur(5px); 41 | transition: .75s; 42 | } 43 | 44 | section.show-animate .animate { 45 | opacity: 1; 46 | filter: blur(0px); 47 | } 48 | 49 | .sec-1 .animate { 50 | margin-top: 200px; 51 | opacity: 0; 52 | } 53 | 54 | .sec-1.show-animate .animate { 55 | opacity: 1; 56 | } 57 | 58 | .logoGif{ 59 | margin-top: -100px; 60 | } 61 | 62 | .sec-2 .animate { 63 | transform: translateX(1000%); 64 | } 65 | 66 | .sec-2.show-animate .animate { 67 | transform: translateX(0); 68 | } 69 | 70 | .sec-3 .animate { 71 | transform: translateX(-1000%); 72 | } 73 | 74 | .sec-3.show-animate .animate { 75 | transform: translateX(0); 76 | } 77 | 78 | .sec-4 .animate { 79 | transform: translateY(300%); 80 | } 81 | 82 | .sec-4.show-animate .animate { 83 | transform: translateY(0); 84 | } 85 | 86 | .sec-5 .images img { 87 | max-width: 250px; 88 | margin: 10px; 89 | border-radius: 30px; 90 | transform: translateX(-1000%); 91 | transition-delay: calc(.2s * var(--i)); 92 | gap: 20px; 93 | } 94 | 95 | .sec-5.show-animate .images img { 96 | transform: translateX(0); 97 | } 98 | 99 | .images { 100 | display: flex; 101 | justify-content: center; 102 | align-items: center; 103 | gap: 10px; 104 | flex-wrap: wrap; 105 | } 106 | 107 | .image-container { 108 | position: relative; 109 | display: inline-block; 110 | } 111 | 112 | .info-box { 113 | visibility: hidden; 114 | width: 500px; /* Allow the box to resize up to a maximum width */ 115 | background-color: #555; 116 | color: #fff; 117 | text-align: center; 118 | border-radius: 6px; 119 | padding: 8px 12px; 120 | position: absolute; 121 | z-index: 1; 122 | bottom: 90%; 123 | left: 50%; 124 | transform: translateX(-50%); 125 | opacity: 0; 126 | transition: opacity 0.3s; 127 | word-wrap: break-word; /* Ensure long words are wrapped */ 128 | white-space: pre-wrap; /* Preserve whitespace */ 129 | box-sizing: border-box; 130 | } 131 | 132 | .info-box::after { 133 | content: ''; 134 | position: absolute; 135 | top: 100%; 136 | left: 50%; 137 | margin-left: -5px; 138 | border-width: 5px; 139 | border-style: solid; 140 | border-color: #555 transparent transparent transparent; 141 | } 142 | 143 | .image-container:hover .info-box { 144 | visibility: visible; 145 | opacity: 1; 146 | } 147 | 148 | .sec-6 .animate { 149 | display: flex; 150 | gap: 10px; 151 | transform: opacity(0); 152 | text-decoration: none; 153 | margin-top: 400px; 154 | } 155 | 156 | .sec-6.show-animate .animate { 157 | transform: opacity(1); 158 | } 159 | 160 | .homepage-buttons{ 161 | font-size: 15px; 162 | color: #407cff; 163 | text-decoration: none; 164 | } 165 | 166 | .no-decoration { 167 | text-decoration: underline; 168 | color: inherit; 169 | } 170 | 171 | .no-decoration:hover { 172 | color: inherit; 173 | text-decoration: none; 174 | } 175 | 176 | #trackedMetricsHeading { 177 | margin-bottom: 20px; 178 | } 179 | 180 | .meetTheTeam { 181 | margin-top: 400px; 182 | } 183 | .team { 184 | display: flex; 185 | flex-direction: row; 186 | justify-content: center; 187 | align-items: center; 188 | gap: 30px; 189 | margin-top: 20px; 190 | margin-bottom: 50px; 191 | } 192 | 193 | .team-member { 194 | display: flex; 195 | flex-direction: column; 196 | justify-content: center; 197 | align-items: center; 198 | gap: 10px; 199 | } 200 | 201 | .name { 202 | font-size: 30px; 203 | margin: 0; 204 | margin-top: 10px; 205 | padding: 0; 206 | } 207 | 208 | .role { 209 | font-family: 'Poppins', sans-serif; 210 | font-size: 20px; 211 | margin: 0; 212 | padding: 0; 213 | } 214 | 215 | .links { 216 | margin-top: 10px; 217 | display: flex; 218 | gap: 15px; 219 | margin-bottom: 80px; 220 | } 221 | 222 | .githubLogo{ 223 | font-size: 35px; 224 | color: white; 225 | } 226 | 227 | .linkedinLogo{ 228 | font-size: 35px; 229 | color: white; 230 | } 231 | 232 | .sec-7{ 233 | display: flex; 234 | margin-top: -100px; 235 | padding: 300px; 236 | margin-bottom: -100px; 237 | } 238 | 239 | .gif { 240 | border-radius: 20px; 241 | } 242 | 243 | .homegif{ 244 | margin-bottom: 200px; 245 | } -------------------------------------------------------------------------------- /src/app/layout.js: -------------------------------------------------------------------------------- 1 | import { Poppins } from "next/font/google"; 2 | import React from "react"; 3 | import "./globals.css"; 4 | import TopNav from "./components/topnav"; 5 | import NextWebVitals from "nextlevelpackage"; 6 | import Script from 'next/script'; 7 | 8 | import SessionWrapper from './components/SessionWrapper' 9 | 10 | const poppins = Poppins({ 11 | subsets: ['latin'], 12 | display: 'swap', 13 | weight: ['200', '400', '600'], 14 | style: ['normal', 'italic'] 15 | }) 16 | 17 | export const metadata = { 18 | title: "NextLevel", 19 | description: "A Next.js performance dashboard", 20 | }; 21 | 22 | export default function RootLayout({ children }) { 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {children} 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/app/lib/auth.js: -------------------------------------------------------------------------------- 1 | import dbConnect from './connectDB'; 2 | import User from '../models/User'; // Adjust the import based on your User model location 3 | import bcrypt from 'bcryptjs'; 4 | 5 | export async function verifyCredentials(username, password) { 6 | // console.log('Connecting to database'); 7 | await dbConnect(); 8 | // console.log('Finding user by username:', username); 9 | const user = await User.findOne({ username }); 10 | 11 | if (!user) { 12 | console.log('User not found'); 13 | return null; 14 | } 15 | 16 | // console.log('User found:', user); 17 | const isPassValid = await bcrypt.compare(password, user.password); // Use your specific method for comparing passwords 18 | if (!isPassValid) { 19 | console.log('Password is invalid'); 20 | return null; 21 | } 22 | 23 | console.log('Password is valid'); 24 | return { 25 | id: user._id, 26 | name: user.name, 27 | email: user.email, 28 | username: user.username, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/app/lib/connectDB.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const MONGODB_URI = process.env.MONGODB_URI; 4 | if (!MONGODB_URI) { 5 | throw new Error('Please define the MONGODB_URI environment variable inside .env'); 6 | } 7 | 8 | let cached = global.mongoose; 9 | 10 | if (!cached) { 11 | cached = global.mongoose = { conn: null, promise: null }; 12 | } 13 | 14 | async function dbConnect() { 15 | if (cached.conn) { 16 | return cached.conn; 17 | } 18 | if (!cached.promise) { 19 | const opts = { 20 | bufferCommands: false, 21 | }; 22 | cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => { 23 | console.log('Database Connected'); 24 | return mongoose; 25 | }); 26 | } 27 | try { 28 | cached.conn = await cached.promise; 29 | } catch (e) { 30 | cached.promise = null; 31 | throw e; 32 | } 33 | 34 | return cached.conn; 35 | } 36 | 37 | module.exports = dbConnect; 38 | 39 | -------------------------------------------------------------------------------- /src/app/login/4k-tech-untb6o7k25k9gvy1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/src/app/login/4k-tech-untb6o7k25k9gvy1.jpg -------------------------------------------------------------------------------- /src/app/login/TransparentIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextLevel/90434dbfb624712e90b31f97414b798ac2cd3e28/src/app/login/TransparentIcon.png -------------------------------------------------------------------------------- /src/app/login/login.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@700&display=swap'); 2 | 3 | :global .login * { 4 | margin: 0; 5 | padding: 0; 6 | font-family: 'Poppins', sans-serif; 7 | height: 100%; 8 | background-color: hsl(240, 7%, 8%); 9 | box-sizing: border-box; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | .body-login { 15 | font-family: 'Poppins', sans-serif; 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | min-height: 100vh; 20 | background: no-repeat; 21 | background-size: cover; 22 | background-position: center; 23 | } 24 | 25 | .wrapper { 26 | width: 420px; 27 | background: transparent; 28 | border: 2px solid rgba(255, 255, 255, 0.2); 29 | backdrop-filter: blur(30px); 30 | box-shadow: 0 0 30px rgba(0,0,0,.2); 31 | color: #fff; 32 | border-radius: 10px; 33 | padding: 30px 40px; 34 | box-shadow: 0 0px 10px rgba(255, 255, 255, 0.308); 35 | } 36 | 37 | .logo-container { 38 | display: flex; 39 | align-items: center; 40 | justify-content: center; 41 | margin-bottom: 20px; /* Adjust as needed */ 42 | } 43 | 44 | .logo { 45 | width: 60px; /* Adjust as needed */ 46 | height: 60px; /* Adjust as needed */ 47 | margin-right: 10px; /* Adjust as needed */ 48 | } 49 | 50 | .wrapper h1 { 51 | font-size: 50px; 52 | text-align: center; 53 | } 54 | 55 | .wrapper .input-box { 56 | position: relative; 57 | width: 100%; 58 | height: 50px; 59 | margin: 30px 0; 60 | } 61 | 62 | .input-box input { 63 | width: 100%; 64 | height: 100%; 65 | background: transparent; 66 | border: none; 67 | outline: none; 68 | border: 2px solid rgba(233, 225, 225, 0.2); 69 | border-radius: 40px; 70 | font-size: 16px; 71 | color: #fff; 72 | padding: 20px 45px 20px 20px; 73 | } 74 | 75 | .input-box input::placeholder { 76 | color: #fff; 77 | } 78 | 79 | .input-box .icon { 80 | position: absolute; 81 | right: 20px; 82 | top: 50%; 83 | transform: translateY(-50%); 84 | font-size: 20px; 85 | } 86 | 87 | input:-webkit-autofill { 88 | -webkit-box-shadow: 0 0 0 1000px transparent inset !important; 89 | -webkit-text-fill-color: #fff !important; 90 | } 91 | 92 | input:-webkit-autofill ~ .icon { 93 | color: #000 !important; 94 | } 95 | 96 | .wrapper .remember-forgot { 97 | display: flex; 98 | justify-content: space-between; 99 | font-size: 14.5px; 100 | margin: -15px 0 15px; 101 | } 102 | 103 | .remember-forgot label input { 104 | accent-color: #fff; 105 | margin-right: 4px; 106 | } 107 | 108 | .remember-forgot a { 109 | color: #fff; 110 | text-decoration: none; 111 | } 112 | 113 | .remember-forgot a:hover { 114 | text-decoration: underline; 115 | } 116 | 117 | .wrapper button { 118 | width: 100%; 119 | height: 45px; 120 | background: #fff; 121 | border: none; 122 | outline: none; 123 | border-radius: 40px; 124 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 125 | cursor: pointer; 126 | font-size: 20px; 127 | color: #333; 128 | font-weight: 700; 129 | } 130 | 131 | .wrapper button:hover { 132 | background-color: rgb(56, 128, 221); 133 | color: white; 134 | } 135 | .oauth-link { 136 | text-align: center; 137 | margin-top: 20px; 138 | margin-bottom: 20px; 139 | text-decoration: none; 140 | } 141 | 142 | .oauth-button { 143 | background-color: #ffffff; 144 | border: 1px solid #dddddd; 145 | padding: 7px; 146 | padding-left: 15px; 147 | gap: 100px !important; 148 | display: inline-block; 149 | width: 140px !important; 150 | align-items: center; 151 | justify-content: center; 152 | cursor: pointer; 153 | font-size: 16px; 154 | color: #333; 155 | margin-right: 10px; 156 | margin-left: 10px; 157 | } 158 | 159 | .google-icon { 160 | margin-right: 8px; 161 | font-size: 30px; 162 | } 163 | .github-icon { 164 | margin-right: 8px; 165 | font-size: 30px; 166 | } 167 | 168 | .wrapper .register-link { 169 | font-size: 14.5px !important; 170 | text-align: center !important; 171 | margin: 20px 0 15px !important; 172 | } 173 | 174 | .wrapper .register-link p { 175 | color: #fff !important; 176 | font-size: 14.5px !important; /* Explicitly setting font size */ 177 | } 178 | 179 | .wrapper .register-link a { 180 | color: #fff !important; 181 | text-decoration: none !important; 182 | font-weight: 600 !important; 183 | font-size: 14.5px !important; /* Explicitly setting font size */ 184 | } 185 | 186 | .wrapper .register-link a:hover { 187 | text-decoration: underline !important; 188 | color: rgb(56, 128, 221) !important; 189 | } 190 | 191 | .message { 192 | text-align: center; 193 | margin-top: 5px; 194 | width: 100%; 195 | margin-bottom: -15px; 196 | } 197 | 198 | 199 | -------------------------------------------------------------------------------- /src/app/login/page.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState, useEffect } from 'react'; 4 | import './login.css'; 5 | import { FaCircleUser } from 'react-icons/fa6'; 6 | import { Si1Password } from 'react-icons/si'; 7 | import { AiOutlineGoogle } from 'react-icons/ai'; 8 | import { IoLogoGithub } from 'react-icons/io'; 9 | import Link from 'next/link'; 10 | import { signIn, useSession } from 'next-auth/react'; 11 | import Spinner from '../components/Spinner.js'; 12 | import Image from 'next/image'; 13 | 14 | export default function Login({ initialLoading = true }) { //added prop for testing purposes 15 | const { data: session, status } = useSession(); 16 | const [username, setUsername] = useState(''); 17 | const [password, setPassword] = useState(''); 18 | const [error, setError] = useState(''); 19 | const [success, setSuccess] = useState(''); 20 | const [loading, setLoading] = useState(initialLoading); // Initially set to true via prop 21 | 22 | useEffect(() => { 23 | document.body.style.fontFamily = "'Poppins', sans-serif"; 24 | document.body.style.display = 'flex'; 25 | document.body.style.justifyContent = 'center'; 26 | document.body.style.alignItems = 'center'; 27 | document.body.style.minHeight = '100vh'; 28 | document.body.style.background = 29 | 'url("https://getwallpapers.com/wallpaper/full/2/8/f/537844.jpg") no-repeat'; 30 | document.body.style.backgroundSize = 'cover'; 31 | document.body.style.backgroundPosition = 'center'; 32 | document.body.style.color = '#fff'; 33 | 34 | // Simulate loading time (for demonstration purposes) 35 | setTimeout(() => { 36 | setLoading(false); // Hide preloader after the component has mounted 37 | }, 4000); // Adjust the timeout as needed 38 | 39 | return () => { 40 | document.body.style.fontFamily = ''; 41 | document.body.style.display = ''; 42 | document.body.style.justifyContent = ''; 43 | document.body.style.alignItems = ''; 44 | document.body.style.minHeight = ''; 45 | document.body.style.background = ''; 46 | document.body.style.backgroundSize = ''; 47 | document.body.style.backgroundPosition = ''; 48 | document.body.style.color = ''; 49 | }; 50 | }, []); 51 | 52 | useEffect(() => { 53 | if (status === 'authenticated' && session) { 54 | const userNameFromSession = session.user.email; 55 | console.log('USERNAME OAUTH', userNameFromSession); 56 | window.location.href = `/dashboard/?username=${encodeURIComponent(userNameFromSession)}`; 57 | } 58 | }, [status, session]); 59 | 60 | const handleSubmit = async (e) => { 61 | e.preventDefault(); 62 | setLoading(true); // Show preloader 63 | // console.log("Submitting login form with username:", username); 64 | 65 | const result = await signIn('credentials', { 66 | redirect: false, 67 | username, 68 | password, 69 | }); 70 | 71 | setLoading(false); // Hide preloader 72 | 73 | if (result?.error) { 74 | console.log('Login error:', result.error); 75 | setError(result.error); 76 | } else { 77 | console.log('Login successful'); 78 | setError(''); 79 | setUsername(''); 80 | setPassword(''); 81 | window.location.href = `/dashboard/?username=${username}`; 82 | } 83 | }; 84 | 85 | const handleOAuthSignIn = async (provider) => { 86 | setLoading(true); // Show preloader 87 | const result = await signIn(provider, { redirect: false }); 88 | // console.log('RESULT', result); 89 | 90 | if (!result?.error) { 91 | const interval = setInterval(() => { 92 | if (status === 'authenticated' && session) { 93 | clearInterval(interval); 94 | const userNameFromSession = session.user.email; 95 | setLoading(false); // Hide preloader 96 | window.location.href = `/dashboard/?username=${encodeURIComponent(userNameFromSession)}`; 97 | } 98 | }, 100); // Check every 100ms 99 | } else { 100 | setLoading(false); // Hide preloader 101 | console.log(`OAuth sign-in error with ${provider}:`, result.error); 102 | setError(`Failed to sign in with ${provider}. Please try again.`); 103 | } 104 | }; 105 | 106 | if (loading) { 107 | return ; 108 | } 109 | 110 | return ( 111 |
112 |
113 |
114 | Logo 115 |

NextLevel

116 |
117 |
118 | setUsername(e.target.value)} 123 | required 124 | /> 125 | 126 |
127 |
128 | setPassword(e.target.value)} 133 | required 134 | /> 135 | 136 |
137 |
138 | 141 | Forgot password? 142 |
143 | 144 | 145 | {error && ( 146 |

147 | {error} 148 |

149 | )} 150 | {success && ( 151 |

152 | Login successful! 153 |

154 | )} 155 |
156 | 164 | 172 |
173 |
181 |

182 | Don't have an account?{' '} 183 | 184 | Register 185 | 186 |

187 |
188 |
189 |
190 | ); 191 | } 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /src/app/models/User.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import bcrypt from 'bcryptjs'; 3 | 4 | const MetricSchema = new mongoose.Schema({ 5 | metricType: { 6 | type: String, 7 | required: [true, 'Metric type was not provided'], 8 | }, 9 | metricValue: { 10 | type: Number, 11 | required: [true, 'Metric value was not provided'], 12 | }, 13 | metricDate: { 14 | type: Date, 15 | required: [true, 'Metric date was not provided'], 16 | default: Date.now, 17 | }, 18 | }); 19 | 20 | const BundleSchema = new mongoose.Schema({ 21 | bundleLog: { 22 | type: String, 23 | required: [true, 'Build log was not provided'], 24 | }, 25 | bundleDate: { 26 | type: Date, 27 | required: [true, 'Build date was not provided'], 28 | default: Date.now, 29 | }, 30 | }); 31 | 32 | const BuildSchema = new mongoose.Schema({ 33 | buildTime: { 34 | type: Number, 35 | required: [true, 'Build time was not provided'], 36 | }, 37 | buildDate: { 38 | type: Date, 39 | required: [true, 'Build date was not provided'], 40 | default: Date.now, 41 | }, 42 | }); 43 | 44 | const UserSchema = new mongoose.Schema({ 45 | username: { 46 | type: String, 47 | required: [true, 'Please provide a username'], 48 | unique: true, 49 | }, 50 | email: { 51 | type: String, 52 | required: [true, 'Please provide a email'], 53 | unique: true, 54 | }, 55 | password: { 56 | type: String, 57 | }, 58 | APIkey: { 59 | type: String, 60 | required: [true, 'API key was not provided'], 61 | unique: true, 62 | }, 63 | image: { 64 | type: String, 65 | default: 'https://www.shutterstock.com/image-vector/user-profile-icon-vector-avatar-600nw-2247726673.jpg', 66 | }, 67 | 68 | FCP: [MetricSchema], 69 | LCP: [MetricSchema], 70 | TTFB: [MetricSchema], 71 | CLS: [MetricSchema], 72 | FID: [MetricSchema], 73 | INP: [MetricSchema], 74 | bundleLog: [BundleSchema], 75 | buildTime: [BuildSchema], 76 | }); 77 | 78 | export default mongoose.models.User || mongoose.model('User', UserSchema, 'Users'); 79 | -------------------------------------------------------------------------------- /src/app/onboarding/Onboarding.module.css: -------------------------------------------------------------------------------- 1 | .onboardingContainer { 2 | padding: 6%; 3 | padding-top: 60px; 4 | background-color: #c9c9c9; 5 | color: black; 6 | } 7 | 8 | .onboardingTitle { 9 | display: flex; 10 | justify-content: center; 11 | color: #000000; 12 | font-size: 30px; 13 | margin-top: 40px; 14 | margin-bottom: 20px; 15 | } 16 | 17 | .step { 18 | margin-bottom: 15px; 19 | padding: 10px; 20 | } 21 | 22 | .stepTitle { 23 | font-weight: bold; 24 | } 25 | 26 | .stepDescription { 27 | margin-top: 5px; 28 | } 29 | 30 | .description { 31 | display: block; 32 | margin-top: 1em; 33 | margin-bottom: 1em; 34 | margin-left: 0; 35 | margin-right: 0; 36 | font-size: 16px; 37 | color: black; 38 | font-weight: 400; 39 | } -------------------------------------------------------------------------------- /src/app/onboarding/api/route.js: -------------------------------------------------------------------------------- 1 | import dbConnect from '../../lib/connectDB'; 2 | import User from '../../models/User'; 3 | import { NextResponse } from 'next/server'; 4 | 5 | 6 | export async function GET(request) { 7 | await dbConnect(); // Ensure database connection 8 | try { 9 | const { searchParams } = new URL(request.nextUrl); 10 | // const { searchParams } = new URL(request.url); 11 | const username = searchParams.get('username'); 12 | if (!username) { 13 | return NextResponse.json({ message: 'Username is required' }, { status: 400, headers: { 'Content-Type': 'application/json' } }); 14 | } 15 | 16 | const foundUser = await User.findOne({ username }); 17 | 18 | if (!foundUser) { 19 | return NextResponse.json({ message: "User not found" }, { status: 404, headers: { 'Content-Type': 'application/json' } }); 20 | } 21 | 22 | return NextResponse.json({ APIkey: foundUser.APIkey }, { status: 200, headers: { 'Content-Type': 'application/json' } }); 23 | } catch (error) { 24 | console.error(error); 25 | return NextResponse.json({ error: error.message }, { status: 500, headers: { 'Content-Type': 'application/json' } }); 26 | } 27 | } 28 | 29 | export const runtime = "nodejs"; -------------------------------------------------------------------------------- /src/app/onboarding/components/CodeBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CopyButton from './CopyButton'; 3 | import styles from './CodeBox.module.css'; 4 | 5 | const CodeBox = ({ fileName, codeText, formattedCode }) => { 6 | return ( 7 |
8 |
9 | {fileName} 10 | 11 |
12 |
13 |
{formattedCode}
14 |
15 |
16 | ); 17 | }; 18 | 19 | export default CodeBox; 20 | -------------------------------------------------------------------------------- /src/app/onboarding/components/CodeBox.module.css: -------------------------------------------------------------------------------- 1 | 2 | .container { 3 | border-radius: 8px; 4 | margin-bottom: 1rem; 5 | } 6 | 7 | .header { 8 | display: flex; 9 | justify-content: space-between; 10 | align-items: center; 11 | background-color: #f5f5f5; 12 | padding: 0.5em 1em; 13 | font-size: 0.9em; 14 | border-bottom: 1px solid #eaeaea; 15 | border-radius: 10px 10px 0px 0px; 16 | } 17 | 18 | .codeArea { 19 | padding: 1em; 20 | background-color: #fff; 21 | overflow-x: auto; 22 | border-radius: 0px 0px 10px 10px; 23 | } 24 | 25 | .copyButton { 26 | background-color: #0070f3; 27 | color: white; 28 | border: none; 29 | padding: 0.3em 0.6em; 30 | border-radius: 4px; 31 | cursor: pointer; 32 | font-size: 0.8em; 33 | } 34 | 35 | .copyButton:hover { 36 | background-color: #005bb5; 37 | } -------------------------------------------------------------------------------- /src/app/onboarding/components/CopyButton.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState } from 'react'; 4 | import styles from './CopyButton.module.css'; 5 | 6 | function CopyButton({ text }) { 7 | const [ copySuccess, setCopySuccess ] = useState(''); 8 | 9 | const copyToClipboard = () => { 10 | navigator.clipboard.writeText(text).then(() => { 11 | setCopySuccess('Copied!'); 12 | setTimeout(() => setCopySuccess(''), 2000); 13 | }, () => { 14 | setCopySuccess('Failed to copy!'); 15 | setTimeout(() => setCopySuccess(''), 2000); 16 | }); 17 | }; 18 | return ( 19 |
20 | 23 | {copySuccess && {copySuccess}} 24 |
25 | ); 26 | } 27 | 28 | export default CopyButton; -------------------------------------------------------------------------------- /src/app/onboarding/components/CopyButton.module.css: -------------------------------------------------------------------------------- 1 | .copyButton { 2 | padding: 10px 20px; 3 | background-color: #224e69; 4 | color: white; 5 | border: none; 6 | border-radius: 4px; 7 | cursor: pointer; 8 | font-size: 14px; 9 | transition: transform 0.1s ease; 10 | } 11 | 12 | .copyButton:hover { 13 | background-color: #90b8cc; 14 | transform: translateY(2px); 15 | } 16 | 17 | .copySuccess { 18 | margin-left: 10px; 19 | color: #143A52; 20 | font-size: 14px; 21 | } -------------------------------------------------------------------------------- /src/app/onboarding/components/NextButton.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import styles from './NextButton.module.css'; 3 | 4 | export default function NextButton(props) { 5 | return ( 6 |
7 | 8 | Continue to Dashboard 9 | 10 |
11 | ); 12 | } -------------------------------------------------------------------------------- /src/app/onboarding/components/NextButton.module.css: -------------------------------------------------------------------------------- 1 | .nextButton { 2 | display: flex; 3 | justify-content: center; 4 | background-color: #2a6f91; 5 | color: white; 6 | padding: 8px 16px; 7 | border-radius: 4px; 8 | text-decoration: none; 9 | position: fixed; 10 | bottom: 30px; 11 | right: 4%; 12 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.273); 13 | transition: background-color 0.3s, box-shadow 0.3s, transform 0.3s; 14 | 15 | } 16 | 17 | .nextButton:hover { 18 | background-color: #8db7cb; 19 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 20 | transform: translateY(2px); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/onboarding/components/Step.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState } from 'react'; 4 | import CodeBox from './CodeBox'; 5 | import styles from '../Onboarding.module.css'; 6 | 7 | const Step = ({ stepNumber, title, description, code, language, api, username }) => { 8 | const [APIkey, setAPIkey] = useState(''); 9 | 10 | useEffect(() => { 11 | if(api === true) { 12 | fetch(`https://www.nextlevel-dash.com/onboarding/api?username=${username}`) 13 | .then((res) => { 14 | if (res.ok) { 15 | // console.log('res:', res); 16 | return res.json(); 17 | } 18 | }) 19 | .then((data) => { 20 | // console.log('API key in onboardin page:', data.APIkey); 21 | setAPIkey(data.APIkey); 22 | }) 23 | .catch((error) => { 24 | console.error('Error fetching API key:', error); 25 | }); 26 | } 27 | }, [username]); 28 | 29 | const formattedCode = code.split('\n').map((line, index) => ( 30 | {line}
31 | )); 32 | 33 | return ( 34 |
35 | {stepNumber &&

Step {stepNumber}: {title}

} 36 |

{description}

37 | 38 | {api === true && } 39 |
40 | ); 41 | }; 42 | 43 | export default Step; -------------------------------------------------------------------------------- /src/app/onboarding/page.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from 'react'; 4 | import Step from './components/Step'; 5 | import styles from './Onboarding.module.css'; 6 | import NextButton from './components/NextButton'; 7 | import withAuth from '../components/withAuth'; 8 | import { useState, useEffect } from 'react'; 9 | 10 | const onboardingSteps = [ 11 | { 12 | stepNumber: 1, 13 | title: "Install and Configure Next.js Bundle Analyzer", 14 | description: "NPM install Next.js Bundle Analyzer:", 15 | code: `npm install @next/bundle-analyzer`, 16 | language: "terminal", 17 | api: false, 18 | }, 19 | { 20 | stepNumber: "", 21 | title: "", 22 | description: "Configure next.config.mjs file:", 23 | code: `import pkg from '@next/bundle-analyzer'; 24 | const withBundleAnalyzer = pkg({ 25 | enabled: process.env.ANALYZE === 'true', 26 | }); 27 | 28 | const nextConfig = {}; 29 | 30 | export default withBundleAnalyzer(nextConfig);`, 31 | language: "next.config.mjs", 32 | api: false, 33 | }, 34 | { 35 | stepNumber: 2, 36 | title: "Install and configure NextLevelPackage", 37 | description: "NPM Install NextLevelPackage:", 38 | code: `npm install nextlevelpackage`, 39 | language: "terminal", 40 | api: false, 41 | }, 42 | { 43 | stepNumber: "", 44 | title: "", 45 | description: "Import NextLevelPackage in layout.js:", 46 | code: `import NextWebVitals from 'nextlevelpackage';`, 47 | language: "layout.js", 48 | api: false, 49 | }, 50 | { 51 | stepNumber: "", 52 | title: "", 53 | description: "Add NextWebVitals component in RootLayout body:", 54 | code: `export default function RootLayout({ children }) { 55 | return ( 56 | 57 | 58 | 59 | {children} 60 | 61 | 62 | ); 63 | }`, 64 | language: "layout.js", 65 | api: false, 66 | }, 67 | { 68 | stepNumber: 3, 69 | title: 'Configure Environment Variables', 70 | description: 'Add the following line to your .env.local file:', 71 | code: `NEXT_PUBLIC_API_KEY=`, 72 | language: '.env.local', 73 | api: true, 74 | }, 75 | { 76 | stepNumber: 4, 77 | title: "Add Build Script to package.json", 78 | description: "Add the following script to your package.json:", 79 | code: `"scripts": { 80 | "nextlevelbuild": "node ./node_modules/nextlevelpackage/cli.js" 81 | }`, 82 | language: "terminal", 83 | api: false, 84 | }, 85 | { 86 | stepNumber: '', 87 | title: '', 88 | description: 'Run this build script instead of \'npm next build\' to track metrics in the dashboard:', 89 | code: `npm run nextlevelbuild`, 90 | language: "terminal", 91 | api: false, 92 | }, 93 | ]; 94 | 95 | function Onboarding () { 96 | const [username, setUsername] = useState(''); 97 | 98 | useEffect(() => { 99 | const currentUrl = window.location.href; 100 | console.log('Current URL:', currentUrl); 101 | const url = new URL(currentUrl); 102 | const usernameFromUrl = url.searchParams.get('username'); 103 | console.log('Username:', usernameFromUrl); 104 | setUsername(usernameFromUrl); 105 | }, []); 106 | 107 | if (!username) { 108 | return null; 109 | } 110 | 111 | return ( 112 |
113 |

114 | NextLevel Onboarding Instructions 115 |

116 | {onboardingSteps.map((step, index) => ( 117 | 128 | ))} 129 | 130 |
131 | ); 132 | }; 133 | 134 | export default withAuth(Onboarding); 135 | -------------------------------------------------------------------------------- /src/app/page.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect } from 'react'; 4 | import './home.css'; 5 | import Link from 'next/link'; 6 | import Image from 'next/image'; 7 | import { IoLogoGithub } from "react-icons/io"; 8 | import { FaLinkedin } from "react-icons/fa6"; 9 | 10 | 11 | export default function Home() { 12 | useEffect(() => { 13 | document.querySelector('.sec-1').classList.add('show-animate'); 14 | 15 | const sections = document.querySelectorAll('section'); 16 | 17 | const handleScroll = () => { 18 | const top = window.scrollY; 19 | sections.forEach((sec) => { 20 | const offset = sec.offsetTop - 300; 21 | const height = sec.offsetHeight; 22 | 23 | if (top >= offset && top < offset + height) { 24 | sec.classList.add('show-animate'); 25 | } 26 | }); 27 | }; 28 | 29 | window.addEventListener('scroll', handleScroll); 30 | return () => { 31 | window.removeEventListener('scroll', handleScroll); 32 | }; 33 | }, []); 34 | 35 | const metricsInfo = { 36 | ttfb: "Time to First Byte measures the time it takes for a user's browser to receive the first byte of page content.", 37 | lcp: "Largest Contentful Paint marks the time at which the largest content element in the viewport is fully rendered.", 38 | fcp: "First Contentful Paint measures the time from when the page starts loading to when any part of the page's content is rendered.", 39 | fid: "First Input Delay measures the time from when a user first interacts with your site to the time when the browser is able to respond to that interaction.", 40 | inp: "Interaction to Next Paint evaluates responsiveness to user interactions by measuring the time taken from user input to the next frame.", 41 | cls: "Cumulative Layout Shift measures the movement of visible elements within the viewport, important for visual stability.", 42 | buildtime: "Build Time is the duration taken to compile and bundle your project's source code.", 43 | bundlesize: "Bundle Size refers to the total size of all the files that are sent to the user's browser." 44 | }; 45 | 46 | return ( 47 |
48 |
49 |
50 |

Take your application to the

51 | NextLevel Logo 52 |
53 | 54 |
55 |

What is NextLevel?

56 |

57 | NextLevel is a performance metrics dashboard tailored to Next.js applications that visualizes critical data, such as build time and key web vitals, enabling developers to pinpoint inefficiencies and improve development productivity and end-user experience. 58 |

59 |
60 | 61 |
62 | NextLevel Demo 63 |
64 |
65 |

STEP ONE

66 | 67 |

Download our npm package

68 | 69 |
70 | 71 |
72 |

STEP TWO

73 |

Connect your Next.js application

74 |
75 | 76 |
77 |

STEP THREE

78 |

Collect and log your data

79 |
80 | 81 |
82 |

Tracked Metrics Include :

83 |
84 | {Object.keys(metricsInfo).slice(0, 4).map((metric, index) => ( 85 |
86 | {metric.toUpperCase()} 87 |
{metricsInfo[metric]}
88 |
89 | ))} 90 |
91 |
92 | {Object.keys(metricsInfo).slice(4).map((metric, index) => ( 93 |
94 | {metric.toUpperCase()} 95 |
{metricsInfo[metric]}
96 |
97 | ))} 98 |
99 |
100 | 101 |
102 |

MEET THE TEAM

103 |

Feel free to contact us if you have any questions!

104 |
105 |
106 | Kim Cuomo Headshot 107 |

Kim Cuomo

108 |

Software Engineer

109 |
110 | 111 | 112 | 113 | 114 | 115 | 116 |
117 |
118 |
119 | Nelly Segimoto Headshot 120 |

Nelly Segimoto

121 |

Software Engineer

122 |
123 | 124 | 125 | 126 | 127 | 128 | 129 |
130 |
131 |
132 | Ian Mann Headshot 133 |

Ian Mann

134 |

Software Engineer

135 |
136 | 137 | 138 | 139 | 140 | 141 | 142 |
143 |
144 |
145 | Frederico Aires Headshot 146 |

Frederico Aires

147 |

Software Engineer

148 |
149 | 150 | 151 | 152 | 153 | 154 | 155 |
156 |
157 |
158 |
159 | 160 |
161 | ); 162 | } 163 | -------------------------------------------------------------------------------- /src/app/signup/page.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | import { useSession, signIn } from 'next-auth/react'; 5 | import './signUp.css'; 6 | import { Si1Password } from 'react-icons/si'; 7 | import { AiOutlineGoogle } from 'react-icons/ai'; 8 | import { IoLogoGithub } from 'react-icons/io'; 9 | import { ImMail4 } from "react-icons/im"; 10 | import Link from 'next/link'; 11 | import Str from '@supercharge/strings'; 12 | 13 | export default function Signup () { 14 | const { data: session, status } = useSession(); 15 | const [username, setUsername] = useState(''); 16 | const [email, setEmail] = useState(''); 17 | const [password, setPassword] = useState(''); 18 | const [APIkey, setAPIkey] = useState(''); 19 | const [confirmPass, setConfirmPass] = useState(''); 20 | const [error, setError] = useState(''); 21 | const [success, setSuccess] = useState(false); 22 | 23 | useEffect(() => { 24 | //Stops background/other css elements from bleeding to next page 25 | document.body.style.fontFamily = "'Poppins', sans-serif"; 26 | document.body.style.display = 'flex'; 27 | document.body.style.justifyContent = 'center'; 28 | document.body.style.alignItems = 'center'; 29 | document.body.style.minHeight = '100vh'; 30 | document.body.style.background = 31 | 'url("https://getwallpapers.com/wallpaper/full/2/8/f/537844.jpg") no-repeat'; 32 | document.body.style.backgroundSize = 'cover'; 33 | document.body.style.backgroundPosition = 'center'; 34 | 35 | return () => { 36 | document.body.style.fontFamily = ''; 37 | document.body.style.display = ''; 38 | document.body.style.justifyContent = ''; 39 | document.body.style.alignItems = ''; 40 | document.body.style.minHeight = ''; 41 | document.body.style.backgroundImage = ''; 42 | document.body.style.backgroundRepeat = 'repeat'; 43 | document.body.style.backgroundSize = 'auto'; 44 | document.body.style.backgroundPosition = '0% 0%'; 45 | }; 46 | }, []); 47 | 48 | useEffect(() => { 49 | if (status === 'authenticated' && session) { 50 | const userNameFromSession = session.user.email; 51 | console.log('USERNAME OAUTH', userNameFromSession); 52 | window.location.href = `/onboarding?username=${encodeURIComponent(userNameFromSession)}`; 53 | } 54 | }, [status, session]); 55 | 56 | const handleSubmit = async (e) => { 57 | e.preventDefault(); 58 | 59 | if (password !== confirmPass) { 60 | setError('Passwords do not match'); 61 | return; 62 | } 63 | 64 | const randomAPI = Str.random(); 65 | setAPIkey(randomAPI); 66 | 67 | try { 68 | const response = await fetch('/api/signup', { 69 | method: 'POST', 70 | headers: { 71 | 'Content-Type': 'application/json', 72 | }, 73 | body: JSON.stringify({ username, email, password, APIkey }), 74 | }); 75 | 76 | if (response.ok) { 77 | setSuccess(true); 78 | setError(''); 79 | setUsername(''); 80 | setEmail(''); 81 | setPassword(''); 82 | setConfirmPass(''); 83 | 84 | // Automatically log in the user 85 | const signInResponse = await signIn('credentials', { 86 | redirect: false, 87 | username, 88 | password 89 | }); 90 | 91 | // console.log('signInResponse:', signInResponse); 92 | 93 | if (signInResponse.ok) { 94 | window.location.href = `/onboarding?username=${username}`; 95 | } else { 96 | setError('Login after signup failed. Please try to login manually.'); 97 | } 98 | } else { 99 | const errorData = await response.json(); 100 | setError(errorData.message); 101 | setSuccess(false); 102 | } 103 | } catch (err) { 104 | console.error('An error occurred:', err); 105 | setError('An error occurred. Please try again.'); 106 | setSuccess(false); 107 | } 108 | }; 109 | 110 | const handleOAuthSignIn = async (provider) => { 111 | const result = await signIn(provider, { redirect: false }); 112 | if (!result.error) { 113 | const interval = setInterval(() => { 114 | if (session) { 115 | clearInterval(interval); 116 | const userNameFromSession = session.user.username || session.user.email; 117 | window.location.href = `/onboarding?username=${encodeURIComponent(userNameFromSession)}`; 118 | } 119 | }, 100); // Check every 100ms 120 | } else { 121 | console.log(`OAuth sign-in error with ${provider}:`, result.error); 122 | setError(`Failed to sign in with ${provider}. Please try again.`); 123 | } 124 | }; 125 | 126 | return ( 127 |
128 |
129 |

Sign Up

130 |
131 | { 136 | setUsername(e.target.value); 137 | setEmail(e.target.value); 138 | }} 139 | required 140 | /> 141 | 142 |
143 |
144 | setPassword(e.target.value)} 149 | required 150 | /> 151 | 152 |
153 |
154 | setConfirmPass(e.target.value)} 159 | required 160 | /> 161 | 162 |
163 | 164 | {error &&

{error}

} 165 | {success && ( 166 |

167 | User registered successfully. 168 |

169 | )} 170 |
171 | 174 | 177 |
178 |
179 |

180 | Already have an account? Login 181 |

182 |
183 |
184 |
185 | ); 186 | } 187 | 188 | 189 | -------------------------------------------------------------------------------- /src/app/signup/signUp.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@700&display=swap'); 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | body { 9 | font-family: 'Poppins', sans-serif; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | min-height: 100vh; 14 | background: no-repeat; 15 | background-size: cover; 16 | background-position: center; 17 | } 18 | 19 | .wrapper { 20 | width: 420px; 21 | background: transparent; 22 | border: 2px solid rgba(255, 255, 255, 0.2); 23 | backdrop-filter: blur(30px); 24 | box-shadow: 0 0 30px rgba(0, 0, 0, 0.2); 25 | color: #fff; 26 | border-radius: 10px; 27 | padding: 30px 40px; 28 | box-shadow: 0 0px 10px rgba(255, 255, 255, 0.308); 29 | } 30 | .wrapper h1 { 31 | font-size: 40px; 32 | text-align: center; 33 | } 34 | 35 | .wrapper .input-box { 36 | position: relative; 37 | width: 100%; 38 | height: 50px; 39 | margin: 30px 0; 40 | 41 | } 42 | 43 | .input-box input { 44 | width: 100%; 45 | height: 100%; 46 | background: transparent; 47 | border: none; 48 | outline: none; 49 | border: 2px solid rgba(233, 225, 225, 0.2); 50 | border-radius: 40px; 51 | font-size: 16px; 52 | color: #fff; 53 | padding: 20px 45px 20px 20px; 54 | /* box-shadow: 0 0px 10px rgba(255, 255, 255, 0.418); */ 55 | } 56 | 57 | .input-box input::placeholder { 58 | color: #fff; 59 | } 60 | 61 | .input-box .icon { 62 | position: absolute; 63 | right: 20px; 64 | top: 50%; 65 | transform: translateY(-50%); 66 | font-size: 20px; 67 | } 68 | 69 | input:-webkit-autofill { 70 | -webkit-box-shadow: 0 0 0 1000px transparent inset !important; 71 | -webkit-text-fill-color: #fff !important; 72 | } 73 | 74 | input:-webkit-autofill ~ .icon { 75 | color: #000 !important; 76 | } 77 | 78 | .wrapper button { 79 | width: 100%; 80 | height: 45px; 81 | background: #fff; 82 | border: none; 83 | outline: none; 84 | border-radius: 40px; 85 | /* box-shadow: 0 0 15px rgba(255, 255, 255, 0.301); */ 86 | cursor: pointer; 87 | font-size: 20px; 88 | color: #333; 89 | font-weight: 700; 90 | } 91 | 92 | .wrapper button:hover { 93 | background-color: rgb(56, 128, 221); 94 | color: white; 95 | } 96 | .oauth-link { 97 | text-align: center; 98 | margin-top: 20px; 99 | margin-bottom: 20px; 100 | text-decoration: none; 101 | } 102 | 103 | .oauth-button { 104 | background-color: #ffffff; 105 | border: 1px solid #dddddd; 106 | padding: 7px; 107 | padding-left: 15px; 108 | gap: 100px !important; 109 | display: inline-block; 110 | width: 140px !important; 111 | align-items: center; 112 | justify-content: center; 113 | cursor: pointer; 114 | font-size: 16px; 115 | color: #333; 116 | margin-right: 10px; 117 | margin-left: 10px; 118 | margin-top: 10px; 119 | /* box-shadow: 0 20px 10px rgb(0 0 0 / 0.2); */ 120 | } 121 | 122 | .google-icon { 123 | margin-right: 8px; 124 | font-size: 30px; 125 | } 126 | 127 | .github-icon { 128 | margin-right: 8px; 129 | font-size: 30px; 130 | } 131 | 132 | .wrapper .register-link { 133 | font-size: 14.5px; 134 | text-align: center; 135 | margin: 20px 0 15px; 136 | } 137 | 138 | .register-link p a { 139 | color: #fff; 140 | text-decoration: none; 141 | font-weight: 600; 142 | } 143 | 144 | .register-link p a:hover { 145 | text-decoration: underline; 146 | color: rgb(56, 128, 221) 147 | } 148 | 149 | .message { 150 | text-align: center; 151 | font-size: 18px; 152 | margin-top: 10px; 153 | width: 100%; 154 | margin-bottom: -20px; 155 | } -------------------------------------------------------------------------------- /src/instrumentation.js: -------------------------------------------------------------------------------- 1 | import dbConnect from './app/lib/connectDB' 2 | 3 | export async function register() { 4 | await dbConnect() 5 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": false, 11 | "noEmit": true, 12 | "incremental": true, 13 | "module": "esnext", 14 | "esModuleInterop": true, 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ] 24 | }, 25 | "include": [ 26 | "next-env.d.ts", 27 | ".next/types/**/*.ts", 28 | "**/*.ts", 29 | "**/*.tsx" 30 | ], 31 | "exclude": [ 32 | "node_modules" 33 | ] 34 | } 35 | --------------------------------------------------------------------------------