├── .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 | 
12 | 
13 | 
14 | 
15 | 
16 | 
17 | 
18 | 
19 | 
20 | 
21 | 
22 | 
23 | 
24 | 
25 | 
26 | 
27 | 
28 | 
29 | 
30 | 
31 | 
32 | 
33 |
34 |
35 |
36 | #
37 |
38 | 
39 | 
40 | 
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 | handleOAuthSignIn('google')}>
14 | Login with Google
15 |
16 | handleOAuthSignIn('github')}>
17 | Login with GitHub
18 |
19 | Close
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 |
10 |
11 | Your browser does not support the video tag.
12 |
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 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Home
27 | Docs
28 | {status === 'authenticated' ? (
29 | <>
30 | Dashboard
31 | Logout
32 | >
33 | ) : (
34 | Login
35 | )}
36 |
37 |
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 |
46 | Copy
47 |
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 |
Download
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 |
Download
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 |
154 | Download
155 |
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 |
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 |
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 |
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 |
21 | Copy
22 |
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 |
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 |
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 |
87 |
{metricsInfo[metric]}
88 |
89 | ))}
90 |
91 |
92 | {Object.keys(metricsInfo).slice(4).map((metric, index) => (
93 |
94 |
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 |
107 |
Kim Cuomo
108 |
Software Engineer
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
Nelly Segimoto
121 |
Software Engineer
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
Ian Mann
134 |
Software Engineer
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
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 |
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 |
--------------------------------------------------------------------------------