├── .gitignore
├── .husky
└── pre-commit
├── LICENSE
├── README.md
├── components.json
├── docker-compose.yml
├── eslint.config.mjs
├── next.config.ts
├── package.json
├── pnpm-lock.yaml
├── postcss.config.mjs
├── prisma
└── schema.prisma
├── public
├── bg
│ ├── hero-1.png
│ ├── hero-gradient.svg
│ ├── hero.png
│ ├── og.png
│ ├── overlay.jpg
│ └── overlay.png
├── file.svg
├── globe.svg
├── icons
│ ├── clock.svg
│ ├── docker.svg
│ ├── github.svg
│ ├── magicpen.svg
│ ├── next-js.svg
│ ├── perk-four.svg
│ ├── perk-one.svg
│ ├── perk-three.svg
│ ├── perk-two.svg
│ ├── postgres.svg
│ ├── project.svg
│ ├── react.svg
│ ├── shield.svg
│ └── tailwind.svg
├── images
│ └── grid-lines.svg
├── logo.png
├── logo.svg
├── next.svg
├── tracking-script.js
├── vercel.svg
├── video
│ ├── hero-1.mp4
│ ├── hero-2.mp4
│ └── hero.mp4
└── window.svg
├── src
├── app
│ ├── (auth)
│ │ └── signin
│ │ │ └── page.tsx
│ ├── (landing)
│ │ ├── _components
│ │ │ ├── cta.tsx
│ │ │ ├── faq.tsx
│ │ │ ├── features.tsx
│ │ │ ├── footer.tsx
│ │ │ ├── hero.tsx
│ │ │ └── navbar.tsx
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── (root)
│ │ ├── layout.tsx
│ │ ├── projects
│ │ │ ├── [website]
│ │ │ │ └── page.tsx
│ │ │ ├── _components
│ │ │ │ ├── analytics-graph.tsx
│ │ │ │ ├── analytics.tsx
│ │ │ │ ├── animated-tab.tsx
│ │ │ │ ├── create-project.tsx
│ │ │ │ ├── empty-project.tsx
│ │ │ │ ├── header.tsx
│ │ │ │ ├── issues.tsx
│ │ │ │ ├── metadata-error.tsx
│ │ │ │ ├── metadata-skeleton.tsx
│ │ │ │ ├── metadata.tsx
│ │ │ │ ├── modal
│ │ │ │ │ ├── create.tsx
│ │ │ │ │ ├── delete.tsx
│ │ │ │ │ └── edit.tsx
│ │ │ │ ├── project-card.tsx
│ │ │ │ ├── project-data.tsx
│ │ │ │ ├── project-skeleton.tsx
│ │ │ │ ├── script.tsx
│ │ │ │ └── website-skeleton.tsx
│ │ │ ├── actions.ts
│ │ │ └── page.tsx
│ │ └── settings
│ │ │ ├── _components
│ │ │ ├── all-logs.tsx
│ │ │ ├── animated-tab.tsx
│ │ │ ├── bug-skeleton.tsx
│ │ │ ├── issues.tsx
│ │ │ ├── log-skeleton.tsx
│ │ │ ├── logs.tsx
│ │ │ └── report-bug.tsx
│ │ │ └── page.tsx
│ ├── api
│ │ ├── auth
│ │ │ └── [...nextauth]
│ │ │ │ └── route.ts
│ │ ├── bug-report
│ │ │ └── route.ts
│ │ ├── logs
│ │ │ └── route.ts
│ │ ├── project
│ │ │ ├── [id]
│ │ │ │ └── route.ts
│ │ │ └── route.ts
│ │ └── track
│ │ │ └── route.ts
│ ├── favicon.ico
│ ├── fonts
│ │ └── satoshi
│ │ │ ├── Satoshi-Black.otf
│ │ │ ├── Satoshi-Bold.otf
│ │ │ ├── Satoshi-Light.otf
│ │ │ ├── Satoshi-Medium.otf
│ │ │ ├── Satoshi-Regular.otf
│ │ │ └── index.ts
│ ├── globals.css
│ ├── layout.tsx
│ ├── opengraph-image.png
│ ├── provider.tsx
│ └── twitter-image.png
├── auth.config.ts
├── auth.ts
├── components
│ ├── globals
│ │ ├── animation-container.tsx
│ │ ├── component-wrapper.tsx
│ │ ├── flashlight-wrapper.tsx
│ │ ├── icons.tsx
│ │ ├── images.tsx
│ │ ├── motion-tab.tsx
│ │ ├── tailwind-indicator.tsx
│ │ └── wrapper.tsx
│ ├── layout
│ │ ├── navbar.tsx
│ │ ├── sidebar-links.tsx
│ │ └── sidebar.tsx
│ └── ui
│ │ ├── accordion.tsx
│ │ ├── avatar.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── dialog.tsx
│ │ ├── input.tsx
│ │ ├── marquee.tsx
│ │ ├── section-badge.tsx
│ │ ├── sheet.tsx
│ │ ├── skeleton.tsx
│ │ ├── sonner.tsx
│ │ ├── tabs.tsx
│ │ └── textarea.tsx
├── config
│ ├── code.tsx
│ ├── faq.ts
│ ├── features.ts
│ ├── nav.ts
│ └── sidebar.ts
├── contexts
│ ├── project-context.tsx
│ └── sidebar-context.tsx
├── data-access
│ └── projects.ts
├── hooks
│ ├── use-click-outside.ts
│ └── use-media-query.ts
├── lib
│ ├── db.ts
│ ├── metadata.ts
│ ├── safe-action.ts
│ ├── session.ts
│ └── utils.ts
├── middleware.ts
├── routes.ts
├── store
│ └── store.ts
├── types
│ └── index.d.ts
└── use-cases
│ └── projects.ts
├── tailwind.config.ts
└── 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.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
43 | # prisma
44 | prisma/data
45 |
46 | # local files
47 | .vscode
48 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | # WEBLYTICS SOFTWARE
2 | # Dual Licensing Agreement
3 | ## Version 2.0, 2025
4 |
5 | This software is available under two distinct licenses:
6 |
7 | 1. GNU Affero General Public License version 3 (AGPL-3.0)
8 | - For open source usage
9 | - Full text available at: https://www.gnu.org/licenses/agpl-3.0.html
10 |
11 | 2. Weblytics Commercial License
12 | - For commercial usage without AGPL-3.0 obligations
13 | - Contact mihirraj444@gmail.com for pricing and terms
14 |
15 | ## PREAMBLE
16 |
17 | Weblytics is committed to maintaining both open source principles
18 | and sustainable business practices. This dual-licensing approach ensures that:
19 | - Open source users retain all freedoms under AGPL-3.0
20 | - Commercial users can obtain flexible licensing terms
21 |
22 | Weblytics provides comprehensive web analytics solutions including:
23 | - Traffic Overview: Clear breakdown of website visitors
24 | - In-Depth Analytics: Actionable insights with detailed reports
25 | - SEO Insights: View essential metadata, OG images, and indexing info
26 | - User Engagement: Track visitor interactions and conversion rates
27 |
28 | ## OPEN SOURCE LICENSE
29 |
30 | For open source usage under AGPL-3.0, the following notice applies:
31 |
32 | Weblytics - Web Analytics Solution
33 | Copyright (C) 2025 Mihir
34 |
35 | This program is free software: you can redistribute it and/or modify
36 | it under the terms of the GNU Affero General Public License as published
37 | by the Free Software Foundation, either version 3 of the License, or
38 | (at your option) any later version.
39 |
40 | This program is distributed in the hope that it will be useful,
41 | but WITHOUT ANY WARRANTY; without even the implied warranty of
42 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
43 | GNU Affero General Public License for more details.
44 |
45 | You should have received a copy of the GNU Affero General Public License
46 | along with this program. If not, see .
47 |
48 | ## COMMERCIAL LICENSE
49 |
50 | The Weblytics Commercial License allows you to use Weblytics for commercial purposes
51 | without the restrictions of the AGPL-3.0 license. This includes:
52 |
53 | 1. The ability to make modifications to the software without being required to
54 | distribute those modifications under the AGPL-3.0.
55 |
56 | 2. The ability to integrate the software with your proprietary systems without
57 | triggering AGPL-3.0 requirements for those systems.
58 |
59 | 3. Priority support and maintenance options.
60 |
61 | 4. Legal indemnification protections.
62 |
63 | For commercial licensing inquiries, please contact:
64 | Email: mihirraj444@gmail.com
65 | GitHub: https://github.com/Mihir2423/analytics
66 |
67 | ## LICENSE COMPLIANCE
68 |
69 | If you are using Weblytics under the AGPL-3.0 license:
70 |
71 | 1. Any modifications you make to the software must be made available under
72 | the same AGPL-3.0 license.
73 |
74 | 2. If you provide network access to functionality of the software (e.g., as a
75 | SaaS offering), you must make the source code available to your users.
76 |
77 | 3. You must include appropriate copyright notices and license information in
78 | your distribution.
79 |
80 | If you are using Weblytics under a Commercial License, you must comply with the
81 | terms of that specific license agreement as provided to you.
82 |
83 | ## NO WARRANTY DISCLAIMER
84 |
85 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
86 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
87 | FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
88 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
89 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
90 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
91 | SOFTWARE.
92 |
93 | ## END OF TERMS AND CONDITIONS
94 |
95 | To apply this license to your Weblytics software, include the following notice at the beginning of each source file:
96 |
97 | Weblytics - Web Analytics Solution
98 | Copyright (C) 2025 Mihir
99 |
100 | This program is dual-licensed under the AGPL-3.0 and the Weblytics Commercial License.
101 |
102 | For open source usage:
103 | This program is free software: you can redistribute it and/or modify
104 | it under the terms of the GNU Affero General Public License as published
105 | by the Free Software Foundation, either version 3 of the License, or
106 | (at your option) any later version.
107 |
108 | This program is distributed in the hope that it will be useful,
109 | but WITHOUT ANY WARRANTY; without even the implied warranty of
110 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
111 | GNU Affero General Public License for more details.
112 |
113 | For commercial licensing options, please contact mihirraj444@gmail.com
114 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2 |
3 | ## Current state
4 |
5 |
6 |
7 | ## Getting Started
8 |
9 | First, run the development server:
10 |
11 | ```bash
12 | npm run dev
13 | # or
14 | yarn dev
15 | # or
16 | pnpm dev
17 | # or
18 | bun dev
19 | ```
20 |
21 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
22 |
23 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
24 |
25 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
26 |
27 | ## Learn More
28 |
29 | To learn more about Next.js, take a look at the following resources:
30 |
31 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
32 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
33 |
34 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
35 |
36 | ## Deploy on Vercel
37 |
38 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
39 |
40 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
41 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | postgres:
3 | image: postgres:15-alpine
4 | ports:
5 | - "5439:5432"
6 | environment:
7 | POSTGRES_USER: postgres
8 | POSTGRES_PASSWORD: postgres
9 | POSTGRES_DB: postgres
10 | volumes:
11 | - ./prisma/data:/var/lib/postgresql/data
12 | volumes:
13 | prisma:
14 | driver: local
15 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | {
15 | ignores: [
16 | // Dependencies
17 | "node_modules/**",
18 |
19 | // Build outputs
20 | "dist/**",
21 | "build/**",
22 | "coverage/**",
23 | ".next/**",
24 | "out/**",
25 |
26 | // Cache directories
27 | ".cache/**",
28 | ".tmp/**",
29 |
30 | // Generated files
31 | "**/*.min.js",
32 | "**/*.bundle.js",
33 |
34 | // Config files
35 | ".github/**",
36 | ".vscode/**",
37 |
38 | // Misc
39 | ".DS_Store",
40 | "*.log",
41 | "public/assets/**",
42 |
43 | // Test fixtures
44 | "fixtures/**",
45 | "test/fixtures/**",
46 | "__mocks__/**",
47 | "tsconfig.json"
48 | ],
49 | },
50 | {
51 | rules: {
52 | "no-unused-vars": "warn",
53 | },
54 | },
55 | ];
56 |
57 | export default eslintConfig;
58 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | /* config options here */
5 | images: {
6 | remotePatterns: [
7 | {
8 | protocol: "https",
9 | hostname: "**",
10 | port: "",
11 | pathname: "**/*",
12 | },
13 | ],
14 | },
15 | };
16 |
17 | export default nextConfig;
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "analytics",
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 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
11 | "lint:fix": "eslint --fix \"**/*.{js,jsx,ts,tsx}\"",
12 | "postinstall": "prisma generate",
13 | "prepare": "husky"
14 | },
15 | "lint-staged": {
16 | "**/*.{js,json}": [
17 | "pnpm run format",
18 | "pnpm run lint:fix",
19 | "git add"
20 | ]
21 | },
22 | "dependencies": {
23 | "@auth/prisma-adapter": "^2.7.4",
24 | "@prisma/client": "^6.3.1",
25 | "@radix-ui/react-accordion": "^1.2.3",
26 | "@radix-ui/react-avatar": "^1.1.3",
27 | "@radix-ui/react-dialog": "^1.1.6",
28 | "@radix-ui/react-slot": "^1.1.2",
29 | "@radix-ui/react-tabs": "^1.1.3",
30 | "axios": "^1.8.1",
31 | "cheerio": "^1.0.0",
32 | "class-variance-authority": "^0.7.1",
33 | "clsx": "^2.1.1",
34 | "date-fns": "^4.1.0",
35 | "framer-motion": "^12.4.5",
36 | "lucide-react": "^0.475.0",
37 | "metadata-scraper": "^0.2.61",
38 | "next": "15.1.7",
39 | "next-auth": "5.0.0-beta.25",
40 | "next-themes": "^0.4.4",
41 | "node-fetch": "^3.3.2",
42 | "rate-limiter-flexible": "^5.0.5",
43 | "react": "^19.0.0",
44 | "react-copy-to-clipboard": "^5.1.0",
45 | "react-dom": "^19.0.0",
46 | "recharts": "^2.15.1",
47 | "shiki": "^3.1.0",
48 | "sonner": "^2.0.1",
49 | "tailwind-merge": "^3.0.1",
50 | "tailwindcss-animate": "^1.0.7",
51 | "ua-parser-js": "^2.0.2",
52 | "zod": "^3.24.2",
53 | "zsa": "^0.6.0",
54 | "zsa-react": "^0.2.3",
55 | "zustand": "^5.0.3"
56 | },
57 | "devDependencies": {
58 | "@eslint/eslintrc": "^3",
59 | "@types/node": "^20",
60 | "@types/react": "^19",
61 | "@types/react-copy-to-clipboard": "^5.0.7",
62 | "@types/react-dom": "^19",
63 | "@types/ua-parser-js": "^0.7.39",
64 | "eslint": "^9",
65 | "eslint-config-next": "15.1.7",
66 | "husky": "^9.1.7",
67 | "postcss": "^8",
68 | "prisma": "^6.3.1",
69 | "tailwindcss": "^3.4.1",
70 | "typescript": "^5"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | datasource db {
2 | provider = "postgresql"
3 | url = env("DATABASE_URL")
4 | // directUrl = env("DIRECT_URL")
5 | }
6 |
7 | generator client {
8 | provider = "prisma-client-js"
9 | }
10 |
11 | model User {
12 | id String @id @default(cuid())
13 | name String?
14 | email String @unique
15 | emailVerified DateTime?
16 | image String?
17 | role String @default("USER")
18 | accounts Account[]
19 | sessions Session[]
20 | projects Project[]
21 | Authenticator Authenticator[]
22 |
23 | createdAt DateTime @default(now())
24 | updatedAt DateTime @updatedAt
25 |
26 | BugReport BugReport[]
27 | }
28 |
29 | model Project {
30 | id String @id @default(cuid())
31 | domain String @unique
32 | name String
33 | description String?
34 | ownerId String
35 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
36 | analytics Analytics?
37 |
38 | createdAt DateTime @default(now())
39 | updatedAt DateTime @updatedAt
40 |
41 | @@index([ownerId])
42 | }
43 |
44 | model Analytics {
45 | id String @id @default(cuid())
46 | projectId String @unique
47 | project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
48 |
49 | totalPageVisits Int @default(0)
50 | totalVisitors Int @default(0)
51 |
52 | visitHistory VisitData[]
53 |
54 | routeAnalytics RouteAnalytics[]
55 |
56 | countryAnalytics CountryAnalytics[]
57 |
58 | deviceAnalytics DeviceAnalytics[]
59 |
60 | osAnalytics OSAnalytics[]
61 |
62 | sourceAnalytics SourceAnalytics[]
63 |
64 | createdAt DateTime @default(now())
65 | updatedAt DateTime @updatedAt
66 | }
67 |
68 | model VisitData {
69 | id String @id @default(cuid())
70 | analyticsId String
71 | analytics Analytics @relation(fields: [analyticsId], references: [id], onDelete: Cascade)
72 |
73 | date DateTime @db.Date
74 | pageVisits Int @default(0)
75 | visitors Int @default(0)
76 |
77 | @@unique([analyticsId, date])
78 | @@index([analyticsId])
79 | @@index([date])
80 | }
81 |
82 | model RouteAnalytics {
83 | id String @id @default(cuid())
84 | analyticsId String
85 | analytics Analytics @relation(fields: [analyticsId], references: [id], onDelete: Cascade)
86 |
87 | route String
88 | visitors Int @default(0)
89 | pageVisits Int @default(0)
90 |
91 | @@unique([analyticsId, route])
92 | @@index([analyticsId])
93 | }
94 |
95 | model CountryAnalytics {
96 | id String @id @default(cuid())
97 | analyticsId String
98 | analytics Analytics @relation(fields: [analyticsId], references: [id], onDelete: Cascade)
99 |
100 | countryCode String @db.VarChar(2)
101 | countryName String
102 | visitors Int @default(0)
103 |
104 | @@unique([analyticsId, countryCode])
105 | @@index([analyticsId])
106 | }
107 |
108 | model DeviceAnalytics {
109 | id String @id @default(cuid())
110 | analyticsId String
111 | analytics Analytics @relation(fields: [analyticsId], references: [id], onDelete: Cascade)
112 |
113 | deviceType DeviceType
114 | visitors Int @default(0)
115 |
116 | @@unique([analyticsId, deviceType])
117 | @@index([analyticsId])
118 | }
119 |
120 | model OSAnalytics {
121 | id String @id @default(cuid())
122 | analyticsId String
123 | analytics Analytics @relation(fields: [analyticsId], references: [id], onDelete: Cascade)
124 |
125 | osName String
126 | visitors Int @default(0)
127 |
128 | @@unique([analyticsId, osName])
129 | @@index([analyticsId])
130 | }
131 |
132 | model SourceAnalytics {
133 | id String @id @default(cuid())
134 | analyticsId String
135 | analytics Analytics @relation(fields: [analyticsId], references: [id], onDelete: Cascade)
136 |
137 | sourceName String
138 | visitors Int @default(0)
139 |
140 | @@unique([analyticsId, sourceName])
141 | @@index([analyticsId])
142 | }
143 |
144 | enum DeviceType {
145 | DESKTOP
146 | MOBILE
147 | TABLET
148 | }
149 |
150 | model Account {
151 | userId String
152 | type String
153 | provider String
154 | providerAccountId String
155 | refresh_token String?
156 | access_token String?
157 | expires_at Int?
158 | token_type String?
159 | scope String?
160 | id_token String?
161 | session_state String?
162 |
163 | createdAt DateTime @default(now())
164 | updatedAt DateTime @updatedAt
165 |
166 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
167 |
168 | @@id([provider, providerAccountId])
169 | }
170 |
171 | model Session {
172 | sessionToken String @unique
173 | userId String
174 | expires DateTime
175 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
176 |
177 | createdAt DateTime @default(now())
178 | updatedAt DateTime @updatedAt
179 | }
180 |
181 | model VerificationToken {
182 | identifier String
183 | token String
184 | expires DateTime
185 |
186 | @@id([identifier, token])
187 | }
188 |
189 | model Authenticator {
190 | credentialID String @unique
191 | userId String
192 | providerAccountId String
193 | credentialPublicKey String
194 | counter Int
195 | credentialDeviceType String
196 | credentialBackedUp Boolean
197 | transports String?
198 |
199 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
200 |
201 | @@id([userId, credentialID])
202 | }
203 |
204 | model Log {
205 | id String @id @default(cuid())
206 | message String
207 | level String @default("info") // "info", "warn", "error"
208 | createdAt DateTime @default(now())
209 | updatedAt DateTime @updatedAt
210 | }
211 |
212 | model BugReport {
213 | id String @id @default(cuid())
214 | title String
215 | description String
216 | status String @default("inReview") // isPending, inReview, inProgress, isResolved
217 | ownerId String
218 | owner User @relation(fields: [ownerId], references: [id])
219 | createdAt DateTime @default(now())
220 | updatedAt DateTime @updatedAt
221 | }
222 |
--------------------------------------------------------------------------------
/public/bg/hero-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mihir2423/analytics/a648ca4619acd1da85267ad831f7851167779982/public/bg/hero-1.png
--------------------------------------------------------------------------------
/public/bg/hero-gradient.svg:
--------------------------------------------------------------------------------
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 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/public/bg/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mihir2423/analytics/a648ca4619acd1da85267ad831f7851167779982/public/bg/hero.png
--------------------------------------------------------------------------------
/public/bg/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mihir2423/analytics/a648ca4619acd1da85267ad831f7851167779982/public/bg/og.png
--------------------------------------------------------------------------------
/public/bg/overlay.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mihir2423/analytics/a648ca4619acd1da85267ad831f7851167779982/public/bg/overlay.jpg
--------------------------------------------------------------------------------
/public/bg/overlay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mihir2423/analytics/a648ca4619acd1da85267ad831f7851167779982/public/bg/overlay.png
--------------------------------------------------------------------------------
/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/clock.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/icons/docker.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/icons/github.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/icons/magicpen.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/public/icons/next-js.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/icons/perk-four.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/public/icons/perk-one.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/icons/perk-three.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/public/icons/perk-two.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/icons/react.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/icons/shield.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/icons/tailwind.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/images/grid-lines.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
11 |
14 |
15 |
17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 |
26 |
27 |
28 |
29 |
31 |
32 |
33 |
34 |
35 |
36 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mihir2423/analytics/a648ca4619acd1da85267ad831f7851167779982/public/logo.png
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/video/hero-1.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mihir2423/analytics/a648ca4619acd1da85267ad831f7851167779982/public/video/hero-1.mp4
--------------------------------------------------------------------------------
/public/video/hero-2.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mihir2423/analytics/a648ca4619acd1da85267ad831f7851167779982/public/video/hero-2.mp4
--------------------------------------------------------------------------------
/public/video/hero.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mihir2423/analytics/a648ca4619acd1da85267ad831f7851167779982/public/video/hero.mp4
--------------------------------------------------------------------------------
/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/(auth)/signin/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { signIn } from "next-auth/react";
5 | import Image from "next/image";
6 | import Link from "next/link";
7 |
8 | const LoginPage = () => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | Weblytics
16 |
17 |
18 |
19 |
Please sign in to continue
20 |
55 |
56 | );
57 | };
58 |
59 | export default LoginPage;
60 |
--------------------------------------------------------------------------------
/src/app/(landing)/_components/cta.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowRightIcon } from "lucide-react";
2 | import Image from "next/image";
3 | import Link from "next/link";
4 | import AnimationContainer from "@/components/globals/animation-container";
5 | import Wrapper from "@/components/globals/wrapper";
6 | import { Button } from "@/components/ui/button";
7 | import SectionBadge from "@/components/ui/section-badge";
8 |
9 | const HIGHLIGHTS = [
10 | {
11 | icon: "/icons/shield.svg",
12 | label: "Accurate Web Analytics",
13 | },
14 | {
15 | icon: "/icons/magicpen.svg",
16 | label: "Comprehensive Website Insights",
17 | },
18 | {
19 | icon: "/icons/clock.svg",
20 | label: "Easy-to-Understand Reports",
21 | },
22 | ];
23 |
24 | const CTA = () => {
25 | return (
26 |
27 |
28 |
29 |
30 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | Take the next step{" "}
50 | today.
51 |
52 |
53 |
54 |
55 |
56 | Unlock powerful tools and insights to grow your business. Start
57 | your journey with us now.
58 |
59 |
60 |
61 |
62 |
63 |
64 | {HIGHLIGHTS.map((item, index) => (
65 |
70 |
71 |
78 |
79 | {item.label}
80 |
81 |
82 |
83 | ))}
84 |
85 |
86 |
87 |
88 |
89 |
90 |
94 | Get Started
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | );
103 | };
104 |
105 | export default CTA;
106 |
--------------------------------------------------------------------------------
/src/app/(landing)/_components/faq.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Accordion,
3 | AccordionContent,
4 | AccordionItem,
5 | AccordionTrigger,
6 | } from "@/components/ui/accordion";
7 | import { FAQS } from "@/config/faq";
8 | import AnimationContainer from "@/components/globals/animation-container";
9 | import Wrapper from "@/components/globals/wrapper";
10 | import SectionBadge from "@/components/ui/section-badge";
11 | import React from "react";
12 |
13 | const FAQ = () => {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Questions? {"We've"} got{" "}
24 | answers.
25 |
26 |
27 |
28 |
29 |
30 | Find answers to common questions about how our platform helps you
31 | analyze SEO data.
32 |
33 |
34 |
35 |
36 |
37 |
38 | {FAQS.map((item, index) => (
39 |
44 |
48 |
49 | {item.question}
50 |
51 |
52 | {item.answer}
53 |
54 |
55 |
56 | ))}
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | export default FAQ;
64 |
--------------------------------------------------------------------------------
/src/app/(landing)/_components/features.tsx:
--------------------------------------------------------------------------------
1 | import { Features } from "@/config/features";
2 | import { cn } from "@/lib/utils";
3 | import React from "react";
4 | import Image from "next/image";
5 | import AnimationContainer from "@/components/globals/animation-container";
6 | import Wrapper from "@/components/globals/wrapper";
7 | import SectionBadge from "@/components/ui/section-badge";
8 |
9 | const FeaturesSection = () => {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Unlock Data-Driven Insights
20 |
21 | with Cutting-Edge
22 | Analytic
23 |
24 |
25 |
26 |
27 |
28 | Track user behavior, monitor key metrics, and optimize performance
29 | effortlessly with real-time analytics.
30 |
31 |
32 |
33 |
34 |
35 |
46 |
47 |
48 | {Features.map((feature, index) => (
49 |
56 |
60 |
61 |
62 |
69 |
70 |
71 |
72 | {feature.title}
73 |
74 |
75 | {feature.description}
76 |
77 |
78 |
79 |
80 |
81 | ))}
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default FeaturesSection;
89 |
--------------------------------------------------------------------------------
/src/app/(landing)/_components/footer.tsx:
--------------------------------------------------------------------------------
1 | import AnimationContainer from "@/components/globals/animation-container";
2 | import React from "react";
3 |
4 | const Footer = () => {
5 | return (
6 |
10 |
11 |
12 | © {new Date().getFullYear()} Weblytics. All rights reserved.
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default Footer;
20 |
--------------------------------------------------------------------------------
/src/app/(landing)/_components/hero.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 | import React from "react";
4 | import AnimationContainer from "@/components/globals/animation-container";
5 | import Wrapper from "@/components/globals/wrapper";
6 | import Images from "@/components/globals/images";
7 | import { Button } from "@/components/ui/button";
8 | import Marquee from "@/components/ui/marquee";
9 | import SectionBadge from "@/components/ui/section-badge";
10 |
11 | const Hero = () => {
12 | const languages = [
13 | Images.comp1,
14 | Images.comp2,
15 | Images.comp3,
16 | Images.comp4,
17 | Images.comp6,
18 | Images.comp7,
19 | Images.comp8,
20 | ];
21 |
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {"Unlock Your Website's Potential"}
34 |
35 |
36 |
37 |
38 |
39 | Unlock powerful insights with ease. Track visitor behavior,
40 | monitor key metrics, and optimize performance effortlessly with
41 | our intuitive analytics platform.
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | Get Started Now
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | Powering Insights with Cutting-Edge Technology
60 |
61 |
62 |
63 | {languages.map((Company, index) => (
64 |
68 |
69 |
70 | ))}
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
84 |
99 |
100 |
101 |
102 | );
103 | };
104 |
105 | export default Hero;
106 |
--------------------------------------------------------------------------------
/src/app/(landing)/_components/navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import AnimationContainer from "@/components/globals/animation-container";
4 | import Wrapper from "@/components/globals/wrapper";
5 | import { cn } from "@/lib/utils";
6 | import { motion } from "framer-motion";
7 | import { Github } from "lucide-react";
8 | import Image from "next/image";
9 | import Link from "next/link";
10 |
11 | const Navbar = () => {
12 | return (
13 |
14 |
19 |
20 |
25 |
26 |
27 |
28 | Weblytics
29 |
30 |
31 |
32 |
33 |
34 |
35 |
40 |
41 |
42 | Star us on GitHub
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default Navbar;
54 |
--------------------------------------------------------------------------------
/src/app/(landing)/layout.tsx:
--------------------------------------------------------------------------------
1 | import Footer from "./_components/footer";
2 | import Navbar from "./_components/navbar";
3 |
4 | export default function PageLayout({
5 | children,
6 | }: {
7 | children: React.ReactNode;
8 | }) {
9 | return (
10 |
11 |
12 | {children}
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/(landing)/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Hero from "./_components/hero";
3 | import Perks from "./_components/features";
4 | import FAQ from "./_components/faq";
5 | import CTA from "./_components/cta";
6 |
7 | const HomePage = () => {
8 | return (
9 |
10 |
17 |
20 |
23 |
26 |
29 |
30 | );
31 | };
32 |
33 | export default HomePage;
34 |
--------------------------------------------------------------------------------
/src/app/(root)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentWrapper } from "@/components/globals/component-wrapper";
2 | import { Navbar } from "@/components/layout/navbar";
3 | import { Sidebar } from "@/components/layout/sidebar";
4 | import { SidebarProvider } from "@/contexts/sidebar-context";
5 | import type { Metadata } from "next";
6 |
7 | export const metadata: Metadata = {
8 | title: "Analytics Dashboard | Web Traffic & Insights",
9 | description:
10 | "Track and analyze website traffic with detailed insights, user behavior metrics, and performance reports.",
11 | };
12 |
13 | export default function RootLayout({
14 | children,
15 | }: Readonly<{
16 | children: React.ReactNode;
17 | }>) {
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {children}
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/[website]/page.tsx:
--------------------------------------------------------------------------------
1 | import { ProjectProvider } from "@/contexts/project-context";
2 | import { getAnalytics } from "@/use-cases/projects";
3 | import { Suspense } from "react";
4 | import { Analytics } from "../_components/analytics";
5 | import { AnimatedTabs } from "../_components/animated-tab";
6 | import { Header } from "../_components/header";
7 | import { Issues } from "../_components/issues";
8 | import { Metadata } from "../_components/metadata";
9 | import { MetadataError } from "../_components/metadata-error";
10 | import { ProjectData } from "../_components/project-data";
11 | import WebsiteDetailSkeleton from "../_components/website-skeleton";
12 |
13 | type Props = {
14 | params: Promise<{ website: string }>;
15 | };
16 |
17 | const WebsiteDetailPage = async ({ params }: Props) => {
18 | const { website } = await params;
19 | const decodedWebsite = decodeURIComponent(website);
20 | return (
21 |
22 | }>
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | const WebsiteDetail = async ({ website }: { website: string }) => {
30 | const websiteData = await getAnalytics(website);
31 | const tabs = [
32 | { id: "metadata", label: "Metadata" },
33 | { id: "analytics", label: "Analytics" },
34 | { id: "issues", label: "Issues" },
35 | ];
36 | return !websiteData ? (
37 |
38 |
39 |
40 | ) : (
41 | <>
42 |
43 |
58 | >
59 | );
60 | };
61 |
62 | export default WebsiteDetailPage;
63 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/_components/analytics-graph.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Line,
3 | ComposedChart,
4 | ResponsiveContainer,
5 | Tooltip,
6 | XAxis,
7 | YAxis,
8 | Area,
9 | } from "recharts";
10 | import { Construction } from "lucide-react";
11 | import { format, parse } from "date-fns";
12 |
13 | const CustomTooltip = ({
14 | active,
15 | payload,
16 | }: {
17 | active: boolean;
18 | payload: { value: number }[];
19 | }) => {
20 | if (active && payload && payload.length) {
21 | return (
22 |
23 |
{`Views: ${payload[0].value}`}
24 |
25 | {`Visitors: ${payload[1].value}`}
26 |
27 |
28 | );
29 | }
30 | return null;
31 | };
32 |
33 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
34 | const AnalyticsGraph = ({ visitHistory }: any) => {
35 | const formatChartData = () => {
36 | if (!visitHistory || visitHistory.length === 0) {
37 | return [];
38 | }
39 |
40 | const sortedVisits = [...visitHistory].sort(
41 | (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
42 | );
43 |
44 | return sortedVisits.map((visit) => ({
45 | name: format(new Date(visit.date), "MMM dd"),
46 | pv: visit.pageVisits,
47 | uv: visit.visitors,
48 | }));
49 | };
50 |
51 | const chartData = formatChartData();
52 |
53 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
54 | const getAdjustedData = (data: any) => {
55 | const dataLength = data.length;
56 | const today = new Date();
57 |
58 | if (dataLength === 0) {
59 | return Array.from({ length: 5 }).map((_, index) => ({
60 | name: format(
61 | new Date(
62 | today.getFullYear(),
63 | today.getMonth(),
64 | today.getDate() - (4 - index),
65 | ),
66 | "MMM dd",
67 | ),
68 | pv: 0,
69 | uv: 0,
70 | }));
71 | }
72 |
73 | if (dataLength < 5) {
74 | const paddedData = [...data];
75 | while (paddedData.length < 5) {
76 | const lastDate =
77 | paddedData.length > 0
78 | ? parse(paddedData[paddedData.length - 1].name, "MMM dd", today)
79 | : new Date(
80 | today.getFullYear(),
81 | today.getMonth(),
82 | today.getDate() - (5 - paddedData.length),
83 | );
84 |
85 | lastDate.setDate(lastDate.getDate() + 1);
86 |
87 | paddedData.push({
88 | name: format(lastDate, "MMM dd"),
89 | pv: 0,
90 | uv: 0,
91 | });
92 | }
93 | return paddedData;
94 | }
95 |
96 | if (dataLength > 5) {
97 | return data.slice(-5);
98 | }
99 |
100 | return data;
101 | };
102 |
103 | const adjustedData = getAdjustedData(chartData);
104 |
105 | if (chartData.length === 0) {
106 | return (
107 |
108 |
109 |
110 |
111 |
112 | No Analytics Data Available
113 |
114 |
115 |
116 | There is no analytics data to display at this time.
117 |
118 |
119 | Check back once you have some visitor activity!
120 |
121 |
122 |
123 |
124 |
125 | );
126 | }
127 |
128 | return (
129 |
130 |
135 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
160 |
165 | } />
166 |
167 | {/* Areas with gradients */}
168 |
169 |
170 |
171 | {/* Lines on top */}
172 |
180 |
187 |
188 |
189 |
190 | );
191 | };
192 |
193 | export { AnalyticsGraph };
194 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/_components/animated-tab.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { MotionTab } from "@/components/globals/motion-tab";
4 | import { useTabStore } from "@/store/store";
5 |
6 | type Props = {
7 | tabs: { id: string; label: string }[];
8 | };
9 |
10 | export function AnimatedTabs({ tabs }: Props) {
11 | const { activeTab, setActiveTab } = useTabStore();
12 |
13 | return (
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/_components/create-project.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useKeyboardShortcut, useModal } from "@/store/store";
4 | import { Command } from "lucide-react";
5 | import React from "react";
6 |
7 | export const CreateProject = () => {
8 | const { onOpen } = useModal();
9 | useKeyboardShortcut();
10 | return (
11 | onOpen("createProject")}
13 | className="flex items-center gap-2 hover:bg-[#5b5b5d38] px-2 py-0 rounded-md h-[32px] font-medium text-white text-xs transition-colors"
14 | >
15 | +
16 | Create Project
17 |
18 | m
19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/_components/empty-project.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { useModal } from "@/store/store";
5 | import Image from "next/image";
6 | import React from "react";
7 |
8 | export const EmptyProject = () => {
9 | const { onOpen } = useModal();
10 | return (
11 |
12 |
13 |
20 |
21 |
Projects
22 |
23 | Add projects and track their analytics in real time. By monitoring
24 | key metrics, you gain valuable insights into performance, helping
25 | you make data-driven decisions to improve and grow your project
26 | effectively.
27 |
28 |
29 |
onOpen("createProject")}
31 | className="bg-[#3d7682] hover:bg-[#3d7782c3] px-6 py-0 w-fit h-8 text-sm"
32 | >
33 | Create new project
34 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/_components/header.tsx:
--------------------------------------------------------------------------------
1 | import { ChevronRight } from "lucide-react";
2 | import React from "react";
3 | import { CreateProject } from "./create-project";
4 | import Link from "next/link";
5 |
6 | type Props = {
7 | project?: string;
8 | title: string;
9 | };
10 |
11 | export const Header = ({ project, title }: Props) => {
12 | return (
13 |
14 |
15 |
16 | {title}
17 |
18 | {project && (
19 |
20 | )}
21 |
22 | {project ? project : ""}
23 |
24 |
25 | {!project &&
}
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/_components/issues.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTabStore } from "@/store/store";
4 | import { Construction } from "lucide-react";
5 | import React from "react";
6 |
7 | export const Issues = () => {
8 | const { activeTab } = useTabStore();
9 | return (
10 |
13 |
14 |
15 |
16 |
17 |
18 | Under Construction
19 |
20 |
21 |
22 | The page is currently under construction.
23 |
24 | Check back soon!
25 |
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/_components/metadata-error.tsx:
--------------------------------------------------------------------------------
1 | import { AlertCircle } from "lucide-react";
2 |
3 | export const MetadataError = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 | Data Not Found
12 |
13 |
14 |
15 | {"We couldn't find any metadata for this website."}
16 |
17 |
18 | Please check the domain and try again.
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/_components/metadata-skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 |
3 | export const MetadataSkeleton = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/_components/metadata.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTabStore } from "@/store/store";
4 | import Image from "next/image";
5 | import { useEffect, useState } from "react";
6 | import { fetchMetadataAction } from "../actions";
7 | import { MetadataSkeleton } from "./metadata-skeleton";
8 | import { MetadataError } from "./metadata-error";
9 | import { useProject } from "@/contexts/project-context";
10 | import { CloudAlert } from "lucide-react";
11 |
12 | type MetadataType = {
13 | title?: string;
14 | description?: string;
15 | image?: string;
16 | favicon?: string;
17 | };
18 |
19 | export const Metadata = ({ domain }: { domain: string }) => {
20 | const { activeTab } = useTabStore();
21 | const [metadata, setMetadata] = useState();
22 | const [loading, setLoading] = useState(false);
23 | const [error, setError] = useState(false);
24 | const { setFavIcon } = useProject();
25 |
26 | useEffect(() => {
27 | const fetchMetadata = async () => {
28 | setLoading(true);
29 | setError(false);
30 | try {
31 | const res = await fetchMetadataAction(domain);
32 | if (res && "data" in res) {
33 | const { data, error } = res;
34 | if (error) {
35 | setError(true);
36 | setMetadata(null);
37 | return;
38 | }
39 | if (data) {
40 | setMetadata({
41 | title: data?.title || "N/A",
42 | description: data?.description || "N/A",
43 | image: data?.image ?? undefined,
44 | });
45 | setFavIcon(data?.favicon || "");
46 | } else {
47 | setError(true);
48 | setMetadata(null);
49 | }
50 | }
51 | } catch (error) {
52 | console.error(error);
53 | setError(true);
54 | setMetadata(null);
55 | } finally {
56 | setLoading(false);
57 | }
58 | };
59 | fetchMetadata();
60 | // eslint-disable-next-line react-hooks/exhaustive-deps
61 | }, [domain]);
62 |
63 | return (
64 |
69 | {loading ? (
70 |
71 | ) : error ? (
72 |
73 | ) : (
74 | <>
75 |
76 |
Title
77 |
{metadata?.title}
78 |
79 |
80 |
Description
81 |
{metadata?.description}
82 |
83 | {metadata?.image ? (
84 |
85 | Opengraph Image
86 |
93 |
94 | ) : (
95 |
96 |
Opengraph Image
97 |
98 |
99 | No OG Image Found
100 |
101 |
102 | )}
103 | >
104 | )}
105 |
106 | );
107 | };
108 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/_components/modal/delete.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Dialog,
5 | DialogContent,
6 | DialogDescription,
7 | DialogFooter,
8 | DialogHeader,
9 | DialogTitle,
10 | } from "@/components/ui/dialog";
11 | import { useModal } from "@/store/store";
12 | import axios from "axios";
13 | import { useRouter } from "next/navigation";
14 | import { useState } from "react";
15 | import { toast } from "sonner";
16 |
17 | export const DeleteModal = () => {
18 | const { isOpen, type, data, onClose } = useModal();
19 | const [deleting, setDeleting] = useState(false);
20 | const router = useRouter();
21 |
22 | const handleDelete = async () => {
23 | try {
24 | setDeleting(true);
25 | const res = await axios.delete(`/api/project/${data.id}`);
26 | if (res.data.success) {
27 | toast.success("Project deleted successfully");
28 | router.refresh();
29 | }
30 | } catch (error) {
31 | console.error("", error);
32 | toast.error("An error occurred while deleting the project");
33 | } finally {
34 | setDeleting(false);
35 | onClose();
36 | }
37 | };
38 |
39 | return (
40 | !open && onClose()}
43 | >
44 |
45 |
46 | Delete Project
47 |
48 | Are you sure you want to delete {data?.name}? This action cannot be
49 | undone.
50 |
51 |
52 |
53 |
54 |
59 | Cancel
60 |
61 |
66 | {deleting ? "Deleting..." : "Delete"}
67 |
68 |
69 |
70 |
71 | );
72 | };
73 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/_components/project-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { useModal } from "@/store/store";
5 | import { FilePenLine, SquareArrowOutUpRight, Trash } from "lucide-react";
6 | import Link from "next/link";
7 | import { useRouter } from "next/navigation";
8 | export const ProjectCard = ({ data }: { data: Project }) => {
9 | const { onOpen } = useModal();
10 | const router = useRouter();
11 |
12 | const encodedDomain = encodeURIComponent(data.domain);
13 | const projectLink = `/projects/${encodedDomain}`;
14 | return (
15 |
16 |
17 |
router.push(projectLink)}
21 | >
22 |
23 |
24 | {data.name}
25 |
26 |
29 |
30 |
34 | {data.domain}
35 |
36 |
37 |
38 | {data?.description ? (
39 |
40 | {data?.description?.length > 60
41 | ? `${data?.description.slice(0, 60)}...`
42 | : data?.description}
43 |
44 | ) : (
45 |
46 | No description provided
47 |
48 | )}
49 |
50 |
51 | onOpen("editProject", data)}
53 | className="bg-transparent hover:bg-transparent p-0 rounded-full text-[#589eaafb]"
54 | >
55 |
56 |
57 | onOpen("deleteProject", data)}
59 | className="bg-transparent hover:bg-transparent p-0 rounded-full text-[#f97171]"
60 | >
61 |
62 |
63 |
64 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/_components/project-data.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import Link from "next/link";
5 | import { useProject } from "@/contexts/project-context";
6 | import { Package, SquareArrowOutUpRight } from "lucide-react";
7 | import Image from "next/image";
8 | type Props = {
9 | website: string;
10 | websiteData: {
11 | name: string | null;
12 | description: string | null;
13 | };
14 | };
15 |
16 | export const ProjectData = ({ website, websiteData }: Props) => {
17 | const { favIcon } = useProject();
18 | return (
19 |
20 | {favIcon && favIcon !== "" ? (
21 |
28 | ) : (
29 |
30 | )}
31 |
{websiteData?.name}
32 |
36 | {website}
37 |
38 |
39 | {websiteData?.description}
40 |
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/_components/project-skeleton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Skeleton } from "@/components/ui/skeleton";
3 |
4 | export const ProjectSkelteon = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/_components/script.tsx:
--------------------------------------------------------------------------------
1 | import { CodeDisplay } from "@/config/code";
2 | import { Copy } from "lucide-react";
3 | import React from "react";
4 |
5 | interface ScriptDisplayProps {
6 | html: string;
7 | onCopy: () => Promise;
8 | }
9 |
10 | export const ScriptDisplay: React.FC = ({
11 | html,
12 | onCopy,
13 | }) => (
14 |
15 |
20 |
21 |
22 |
23 |
24 | );
25 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/_components/website-skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Package } from "lucide-react";
2 | import { Skeleton } from "@/components/ui/skeleton";
3 | import { AnimatedTabs } from "./animated-tab";
4 |
5 | const WebsiteDetailSkeleton = () => {
6 | const tabs = [
7 | { id: "metadata", label: "Metadata" },
8 | { id: "analytics", label: "Analytics" },
9 | { id: "issues", label: "Issues" },
10 | ];
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default WebsiteDetailSkeleton;
46 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/actions.ts:
--------------------------------------------------------------------------------
1 | // src/app/api/metadata/actions.ts
2 | "use server";
3 |
4 | import { extractMetadata } from "@/lib/metadata";
5 |
6 | export async function fetchMetadataAction(domain: string) {
7 | try {
8 | const data = await extractMetadata(`https://${domain}`);
9 | if (data.error) {
10 | return null;
11 | }
12 | return data;
13 | } catch (error) {
14 | return { error };
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/(root)/projects/page.tsx:
--------------------------------------------------------------------------------
1 | import { assertAuthenticated } from "@/lib/session";
2 | import { getAllProjects } from "@/use-cases/projects";
3 | import { Suspense } from "react";
4 | import { EmptyProject } from "./_components/empty-project";
5 | import { Header } from "./_components/header";
6 | import { CreateModal } from "./_components/modal/create";
7 | import { DeleteModal } from "./_components/modal/delete";
8 | import { EditModal } from "./_components/modal/edit";
9 | import { ProjectCard } from "./_components/project-card";
10 | import { ProjectSkelteon } from "./_components/project-skeleton";
11 |
12 | const ProjectsPage = async () => {
13 | return (
14 |
15 |
16 |
}>
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | };
25 |
26 | const Projects = async () => {
27 | const session = await assertAuthenticated();
28 | const projects = await getAllProjects(session.id);
29 | return projects && Array.isArray(projects) && projects.length > 0 ? (
30 |
31 |
32 | {projects.map((data, index) => (
33 |
34 | ))}
35 |
36 |
37 | ) : (
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default ProjectsPage;
45 |
--------------------------------------------------------------------------------
/src/app/(root)/settings/_components/all-logs.tsx:
--------------------------------------------------------------------------------
1 | import { getAllLogs } from "@/use-cases/projects";
2 | import { Suspense } from "react";
3 | import { Logs } from "./logs";
4 | import { LogsSkeleton } from "./log-skeleton";
5 |
6 | export default async function AllLogs() {
7 | const logs = await getAllLogs();
8 | return (
9 | }>
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/(root)/settings/_components/animated-tab.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { MotionTab } from "@/components/globals/motion-tab";
4 | import { useSettingsTabStore } from "@/store/store";
5 |
6 | type Props = {
7 | tabs: { id: string; label: string }[];
8 | };
9 |
10 | export function AnimatedTabs({ tabs }: Props) {
11 | const { activeTab, setActiveTab } = useSettingsTabStore();
12 |
13 | return (
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/(root)/settings/_components/bug-skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 |
3 | export const BugReportSkeleton = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/app/(root)/settings/_components/issues.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardFooter,
9 | CardHeader,
10 | CardTitle,
11 | } from "@/components/ui/card";
12 | import { useSettingsTabStore } from "@/store/store";
13 | import { Bug } from "lucide-react";
14 | import { useEffect, useState } from "react";
15 | import { useModal } from "@/store/store";
16 | import { CreateBugReportModal } from "./report-bug";
17 | import { BugReportSkeleton } from "./bug-skeleton";
18 |
19 | export const Issues = () => {
20 | const { activeTab } = useSettingsTabStore();
21 | const { onOpen } = useModal();
22 | interface BugReport {
23 | id: string;
24 | title: string;
25 | createdAt: string;
26 | status: "isResolved" | "inProgress" | "isPending";
27 | }
28 |
29 | const [bugReports, setBugReports] = useState([]);
30 | const [loading, setLoading] = useState(true);
31 |
32 | // Fetch bug reports
33 | useEffect(() => {
34 | const fetchBugReports = async () => {
35 | try {
36 | const res = await fetch("/api/bug-report");
37 | const data = await res.json();
38 | if (data.success) {
39 | setBugReports(data.bugReports);
40 | } else {
41 | console.error("Failed to fetch bug reports:", data.message);
42 | }
43 | } catch (error) {
44 | console.error("Error fetching bug reports:", error);
45 | } finally {
46 | setLoading(false);
47 | }
48 | };
49 |
50 | fetchBugReports();
51 | }, []);
52 |
53 | return (
54 |
59 |
60 | Bug Reports
61 | Report issues and track their status.
62 |
63 |
64 | {loading ? (
65 | <>
66 |
67 |
68 |
69 | >
70 | ) : bugReports && Array.isArray(bugReports) && bugReports.length > 0 ? (
71 | bugReports.map((report) => (
72 |
76 |
77 |
78 |
89 |
90 |
91 | {report.title}
92 |
93 |
94 | Submitted on{" "}
95 | {new Date(report.createdAt).toLocaleDateString()}
96 |
97 |
98 |
99 |
108 | {report.status === "inProgress"
109 | ? "In Progress"
110 | : report.status.substring(2)}
111 |
112 |
113 |
114 | ))
115 | ) : (
116 |
117 |
118 | No bug reports found.
119 |
120 | )}
121 |
122 |
123 | onOpen("createBugReport")}
125 | className="bg-[#C05D5D] hover:bg-[#c05d5dcb]"
126 | >
127 | Report New Bug
128 |
129 |
130 |
131 |
132 | );
133 | };
134 |
--------------------------------------------------------------------------------
/src/app/(root)/settings/_components/log-skeleton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Skeleton } from "@/components/ui/skeleton";
4 |
5 | import {
6 | Card,
7 | CardContent,
8 | CardDescription,
9 | CardHeader,
10 | CardTitle,
11 | } from "@/components/ui/card";
12 | import { useSettingsTabStore } from "@/store/store";
13 | export const LogsSkeleton = () => {
14 | const { activeTab } = useSettingsTabStore();
15 |
16 | return (
17 |
22 |
23 | Product Log
24 |
25 | Admin-generated log of product updates, maintenance, and changes.
26 |
27 |
28 |
29 | {[...Array(3)].map((_, i) => (
30 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | ))}
42 |
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/src/app/(root)/settings/_components/logs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Card,
5 | CardContent,
6 | CardDescription,
7 | CardHeader,
8 | CardTitle,
9 | } from "@/components/ui/card";
10 | import { useSettingsTabStore } from "@/store/store";
11 | import { AlertCircle, Bell, CloudAlert } from "lucide-react";
12 |
13 | // Map log levels to colors
14 | const logLevelColors = {
15 | info: "#4ECDC4",
16 | warn: "#FFD166",
17 | error: "#FF6B6B",
18 | };
19 |
20 | // Map log levels to icons
21 | const logLevelIcons = {
22 | info: Bell,
23 | warn: AlertCircle,
24 | error: CloudAlert,
25 | };
26 |
27 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
28 | export const Logs = ({ logs }: { logs: any[] }) => {
29 | const { activeTab } = useSettingsTabStore();
30 |
31 | return (
32 |
37 |
38 | Product Log
39 |
40 | Admin-generated log of product updates, maintenance, and changes.
41 |
42 |
43 |
44 | {Array.isArray(logs) && logs.length > 0 ? (
45 | logs.map((entry, i) => {
46 | const IconComponent =
47 | logLevelIcons[entry.level as keyof typeof logLevelIcons] || Bell;
48 | const color =
49 | logLevelColors[entry.level as keyof typeof logLevelColors] ||
50 | "#45B6FE";
51 | return (
52 |
56 |
60 |
61 |
62 |
63 |
{entry.message}
64 |
65 | {entry.user?.email || "System"}
66 |
67 |
68 | {new Date(entry.createdAt).toLocaleString()}
69 |
70 |
71 |
72 | );
73 | })
74 | ) : (
75 |
76 |
77 | No Data Found
78 |
79 | )}
80 |
81 |
82 | );
83 | };
84 |
--------------------------------------------------------------------------------
/src/app/(root)/settings/_components/report-bug.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Dialog,
5 | DialogContent,
6 | DialogFooter,
7 | DialogHeader,
8 | DialogTitle,
9 | } from "@/components/ui/dialog";
10 | import { toast } from "sonner";
11 | import axios from "axios";
12 | import { Input } from "@/components/ui/input";
13 | import { Textarea } from "@/components/ui/textarea";
14 | import { useModal } from "@/store/store";
15 | import { Bug } from "lucide-react";
16 | import { useState, useCallback, memo, useEffect } from "react";
17 | import { useRouter } from "next/navigation";
18 |
19 | export const CreateBugReportModal = memo(() => {
20 | const { isOpen, type, onClose } = useModal();
21 | const router = useRouter();
22 | const [data, setData] = useState({
23 | title: "",
24 | description: "",
25 | });
26 | const [creating, setCreating] = useState(false);
27 |
28 | const handleChange = useCallback(
29 | (e: React.ChangeEvent) => {
30 | setData((prevData) => ({
31 | ...prevData,
32 | [e.target.name]: e.target.value,
33 | }));
34 | },
35 | [],
36 | );
37 |
38 | const onSubmit = useCallback(async () => {
39 | if (!data.title || !data.description) {
40 | return toast.error("Please fill all the fields");
41 | }
42 | setCreating(true);
43 | try {
44 | const res = await axios.post("/api/bug-report", data);
45 | if (!res.data.success) {
46 | return toast.error(res.data.message);
47 | }
48 | toast.success("Bug report in review.");
49 | router.refresh();
50 | onClose();
51 | } catch (error) {
52 | console.error("Error creating bug report:", error);
53 | toast.error("Failed to create bug report");
54 | } finally {
55 | setCreating(false);
56 | }
57 | }, [data, onClose, router]);
58 |
59 | useEffect(() => {
60 | const handleKeyPress = (e: KeyboardEvent) => {
61 | if (e.key === "Enter") {
62 | onSubmit();
63 | }
64 | };
65 | document.addEventListener("keydown", handleKeyPress);
66 | return () => document.removeEventListener("keydown", handleKeyPress);
67 | }, [onSubmit]);
68 |
69 | return (
70 | !open && onClose()}
73 | >
74 |
75 |
76 |
77 | Report New Bug
78 |
79 |
80 |
81 |
82 |
83 |
84 |
92 |
93 |
94 |
102 |
103 |
104 |
105 |
106 |
110 | Cancel
111 |
112 |
116 | {creating ? "Creating..." : "Create Report"}
117 |
118 |
119 |
120 |
121 | );
122 | });
123 |
124 | CreateBugReportModal.displayName = "CreateBugReportModal";
125 |
--------------------------------------------------------------------------------
/src/app/(root)/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 | import AllLogs from "./_components/all-logs";
3 | import { AnimatedTabs } from "./_components/animated-tab";
4 | import { Issues } from "./_components/issues";
5 |
6 | const SettingsPage = () => {
7 | const tabs = [
8 | { id: "logs", label: "Logs" },
9 | { id: "issues", label: "Issues" },
10 | ];
11 | return (
12 |
13 |
14 |
15 |
Settings
16 |
17 | Manage your account settings and preferences.
18 |
19 |
20 |
21 |
Loading logs... }>
22 |
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default SettingsPage;
31 |
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import { handlers } from "@/auth";
2 |
3 | export const { GET, POST } = handlers;
4 |
--------------------------------------------------------------------------------
/src/app/api/bug-report/route.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@/auth";
2 | import prisma from "@/lib/db";
3 | import { revalidatePath } from "next/cache";
4 | import { NextResponse } from "next/server";
5 |
6 | export async function POST(req: Request) {
7 | try {
8 | const session = await auth();
9 |
10 | if (!session) {
11 | return NextResponse.json(
12 | { message: "Unauthorized", success: false },
13 | { status: 403 },
14 | );
15 | }
16 |
17 | const { title, description } = await req.json();
18 |
19 | if (!title || !description) {
20 | return NextResponse.json(
21 | { message: "Title and description are required", success: false },
22 | { status: 400 },
23 | );
24 | }
25 |
26 | const bugReport = await prisma.bugReport.create({
27 | data: {
28 | title,
29 | description,
30 | ownerId: session.user.id,
31 | },
32 | });
33 | revalidatePath("/settings");
34 |
35 | return NextResponse.json(
36 | { bugReport, message: "Bug report created successfully", success: true },
37 | { status: 201 },
38 | );
39 | } catch (error) {
40 | console.error("Error creating bug report:", error);
41 | return NextResponse.json(
42 | { message: "Internal Server Error", success: false, error },
43 | { status: 500 },
44 | );
45 | }
46 | }
47 |
48 | export async function GET() {
49 | try {
50 | const session = await auth();
51 |
52 | if (!session) {
53 | return NextResponse.json(
54 | { message: "Unauthorized", success: false },
55 | { status: 403 },
56 | );
57 | }
58 |
59 | const bugReports = await prisma.bugReport.findMany({
60 | where: {
61 | ownerId: session.user.id,
62 | status: {
63 | not: "inReview",
64 | },
65 | },
66 | orderBy: {
67 | createdAt: "desc",
68 | },
69 | });
70 |
71 | return NextResponse.json(
72 | {
73 | bugReports,
74 | message: "Bug reports fetched successfully",
75 | success: true,
76 | },
77 | { status: 200 },
78 | );
79 | } catch (error) {
80 | console.error("Error fetching bug reports:", error);
81 | return NextResponse.json(
82 | { message: "Internal Server Error", success: false },
83 | { status: 500 },
84 | );
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/app/api/logs/route.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@/auth";
2 | import prisma from "@/lib/db";
3 | import { NextResponse } from "next/server";
4 |
5 | export async function POST(req: Request) {
6 | try {
7 | const session = await auth();
8 |
9 | // Check if the user is authenticated and is an admin
10 | if (!session || session.user.role !== "admin") {
11 | return NextResponse.json(
12 | {
13 | message: "Unauthorized: Only admins can create logs",
14 | success: false,
15 | },
16 | { status: 403 },
17 | );
18 | }
19 |
20 | // Parse the request body
21 | const { message, level = "info" } = await req.json();
22 |
23 | // Validate the input
24 | if (!message) {
25 | return NextResponse.json(
26 | { message: "Message is required", success: false },
27 | { status: 400 },
28 | );
29 | }
30 |
31 | // Create the log
32 | const log = await prisma.log.create({
33 | data: {
34 | message,
35 | level,
36 | },
37 | });
38 |
39 | return NextResponse.json(
40 | { log, message: "Log created successfully", success: true },
41 | { status: 201 },
42 | );
43 | } catch (error) {
44 | console.error("Error creating log:", error);
45 | return NextResponse.json(
46 | { message: "Internal Server Error", success: false },
47 | { status: 500 },
48 | );
49 | }
50 | }
51 |
52 | export async function GET() {
53 | try {
54 | const session = await auth();
55 |
56 | // Check if the user is authenticated
57 | if (!session) {
58 | return NextResponse.json(
59 | { message: "Unauthorized", success: false },
60 | { status: 403 },
61 | );
62 | }
63 |
64 | // Fetch all logs
65 | const logs = await prisma.log.findMany({
66 | orderBy: {
67 | createdAt: "desc", // Sort logs by creation date (newest first)
68 | },
69 | });
70 |
71 | return NextResponse.json(
72 | { logs, message: "Logs fetched successfully", success: true },
73 | { status: 200 },
74 | );
75 | } catch (error) {
76 | console.error("Error fetching logs:", error);
77 | return NextResponse.json(
78 | { message: "Internal Server Error", success: false },
79 | { status: 500 },
80 | );
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/app/api/project/[id]/route.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@/auth";
2 | import prisma from "@/lib/db";
3 | import { revalidatePath, revalidateTag } from "next/cache";
4 | import { NextResponse } from "next/server";
5 |
6 | export async function DELETE(
7 | request: Request,
8 | { params }: { params: Promise<{ id: string }> },
9 | ) {
10 | const { id } = await params;
11 |
12 | try {
13 | const session = await auth();
14 | if (!session) {
15 | return NextResponse.json(
16 | { user: null, message: "Unauthorized", success: false },
17 | { status: 403 },
18 | );
19 | }
20 |
21 | const deletedProject = await prisma.project.deleteMany({
22 | where: { id, ownerId: session.user.id },
23 | });
24 |
25 | if (deletedProject.count === 0) {
26 | return NextResponse.json(
27 | { message: "Project not found or unauthorized", success: false },
28 | { status: 404 },
29 | );
30 | }
31 |
32 | revalidateTag("projects");
33 | revalidatePath("/projects");
34 |
35 | return NextResponse.json(
36 | { message: "Project deleted", success: true },
37 | { status: 200 },
38 | );
39 | } catch (error) {
40 | console.error("Error deleting project:", error);
41 | return NextResponse.json(
42 | { message: "Internal Server Error", success: false },
43 | { status: 500 },
44 | );
45 | }
46 | }
47 |
48 | export async function PATCH(
49 | req: Request,
50 | { params }: { params: Promise<{ id: string }> },
51 | ) {
52 | const { id } = await params;
53 | try {
54 | const session = await auth();
55 |
56 | if (!session) {
57 | return NextResponse.json(
58 | { user: null, message: "Unauthorized", success: false },
59 | { status: 403 },
60 | );
61 | }
62 | const values = await req.json();
63 |
64 | if (!values.domain || !values.name || !values.description) {
65 | return NextResponse.json(
66 | { message: "Missing required fields", success: false },
67 | { status: 400 },
68 | );
69 | }
70 |
71 | // Check if the project exists and belongs to the user
72 | const existingProject = await prisma.project.findFirst({
73 | where: { id, ownerId: session.user.id },
74 | });
75 |
76 | if (!existingProject) {
77 | return NextResponse.json(
78 | { message: "Project not found or unauthorized", success: false },
79 | { status: 404 },
80 | );
81 | }
82 |
83 | // Update the project
84 | const updatedProject = await prisma.project.update({
85 | where: { id },
86 | data: {
87 | domain: values.domain,
88 | name: values.name,
89 | description: values.description,
90 | },
91 | });
92 |
93 | // Revalidate the cache
94 | revalidateTag("projects");
95 | revalidatePath("/projects");
96 |
97 | return NextResponse.json(
98 | { project: updatedProject, message: "Project updated", success: true },
99 | { status: 200 },
100 | );
101 | } catch (error) {
102 | console.error("Error updating project:", error);
103 | return NextResponse.json(
104 | { message: "Internal Server Error", success: false },
105 | { status: 500 },
106 | );
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/app/api/project/route.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@/auth";
2 | import prisma from "@/lib/db";
3 | import { revalidatePath, revalidateTag } from "next/cache";
4 | import { NextResponse } from "next/server";
5 |
6 | export async function POST(req: Request) {
7 | try {
8 | const session = await auth();
9 |
10 | if (!session) {
11 | return NextResponse.json(
12 | { user: null, message: "Unauthorized", success: false },
13 | { status: 403 }
14 | );
15 | }
16 |
17 | const values = await req.json();
18 | if (!values.domain || !values.name || !values.description) {
19 | return NextResponse.json(
20 | { message: "Missing required fields", success: false },
21 | { status: 400 }
22 | );
23 | }
24 | const existingProject = await prisma.project.findFirst({
25 | where: { domain: values.domain },
26 | });
27 | if (existingProject) {
28 | return NextResponse.json(
29 | { message: "Domain already exists", success: false },
30 | { status: 400 }
31 | );
32 | }
33 |
34 | const project = await prisma.project.create({
35 | data: {
36 | domain: values.domain,
37 | name: values.name,
38 | description: values.description,
39 | owner: { connect: { id: session.user.id } },
40 | },
41 | });
42 |
43 | revalidateTag("projects");
44 | revalidatePath("/projects");
45 |
46 | return NextResponse.json(
47 | { project, message: "Project created", success: true },
48 | { status: 201 }
49 | );
50 | } catch (error) {
51 | console.error("Error creating project:", error);
52 |
53 | return NextResponse.json(
54 | { message: "Internal Server Error", success: false },
55 | { status: 500 }
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mihir2423/analytics/a648ca4619acd1da85267ad831f7851167779982/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/fonts/satoshi/Satoshi-Black.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mihir2423/analytics/a648ca4619acd1da85267ad831f7851167779982/src/app/fonts/satoshi/Satoshi-Black.otf
--------------------------------------------------------------------------------
/src/app/fonts/satoshi/Satoshi-Bold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mihir2423/analytics/a648ca4619acd1da85267ad831f7851167779982/src/app/fonts/satoshi/Satoshi-Bold.otf
--------------------------------------------------------------------------------
/src/app/fonts/satoshi/Satoshi-Light.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mihir2423/analytics/a648ca4619acd1da85267ad831f7851167779982/src/app/fonts/satoshi/Satoshi-Light.otf
--------------------------------------------------------------------------------
/src/app/fonts/satoshi/Satoshi-Medium.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mihir2423/analytics/a648ca4619acd1da85267ad831f7851167779982/src/app/fonts/satoshi/Satoshi-Medium.otf
--------------------------------------------------------------------------------
/src/app/fonts/satoshi/Satoshi-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mihir2423/analytics/a648ca4619acd1da85267ad831f7851167779982/src/app/fonts/satoshi/Satoshi-Regular.otf
--------------------------------------------------------------------------------
/src/app/fonts/satoshi/index.ts:
--------------------------------------------------------------------------------
1 | import localFont from "next/font/local";
2 |
3 | export const satoshi = localFont({
4 | src: [
5 | {
6 | path: "./Satoshi-Black.otf",
7 | weight: "900",
8 | style: "black",
9 | },
10 | {
11 | path: "./Satoshi-Bold.otf",
12 | weight: "700",
13 | style: "bold",
14 | },
15 | {
16 | path: "./Satoshi-Medium.otf",
17 | weight: "500",
18 | style: "medium",
19 | },
20 | {
21 | path: "./Satoshi-Regular.otf",
22 | weight: "400",
23 | style: "regular",
24 | },
25 | {
26 | path: "./Satoshi-Light.otf",
27 | weight: "300",
28 | style: "light",
29 | },
30 | ],
31 | });
32 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 20 14.3% 4.1%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 20 14.3% 4.1%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 20 14.3% 4.1%;
13 | --primary: 24.6 95% 53.1%;
14 | --primary-foreground: 60 9.1% 97.8%;
15 | --secondary: 60 4.8% 95.9%;
16 | --secondary-foreground: 24 9.8% 10%;
17 | --muted: 60 4.8% 95.9%;
18 | --muted-foreground: 25 5.3% 44.7%;
19 | --accent: 60 4.8% 95.9%;
20 | --accent-foreground: 24 9.8% 10%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 60 9.1% 97.8%;
23 | --border: 20 5.9% 90%;
24 | --input: 20 5.9% 90%;
25 | --ring: 24.6 95% 53.1%;
26 | --radius: 0.5rem;
27 | --chart-1: 12 76% 61%;
28 | --chart-2: 173 58% 39%;
29 | --chart-3: 197 37% 24%;
30 | --chart-4: 43 74% 66%;
31 | --chart-5: 27 87% 67%;
32 | }
33 |
34 | .dark {
35 | --background: 20 14.3% 4.1%;
36 | --foreground: 60 9.1% 97.8%;
37 | --card: 20 14.3% 4.1%;
38 | --card-foreground: 60 9.1% 97.8%;
39 | --popover: 20 14.3% 4.1%;
40 | --popover-foreground: 60 9.1% 97.8%;
41 | --primary: 20.5 90.2% 48.2%;
42 | --primary-foreground: 60 9.1% 97.8%;
43 | --secondary: 12 6.5% 15.1%;
44 | --secondary-foreground: 60 9.1% 97.8%;
45 | --muted: 12 6.5% 15.1%;
46 | --muted-foreground: 24 5.4% 63.9%;
47 | --accent: 12 6.5% 15.1%;
48 | --accent-foreground: 60 9.1% 97.8%;
49 | --destructive: 0 72.2% 50.6%;
50 | --destructive-foreground: 60 9.1% 97.8%;
51 | --radius: 0.6rem;
52 | --border: 12 6.5% 15.1%;
53 | --input: 12 6.5% 15.1%;
54 | --ring: 20.5 90.2% 48.2%;
55 | --chart-1: 220 70% 50%;
56 | --chart-2: 160 60% 45%;
57 | --chart-3: 30 80% 55%;
58 | --chart-4: 280 65% 60%;
59 | --chart-5: 340 75% 55%;
60 | }
61 | }
62 |
63 | @layer base {
64 | * {
65 | @apply border-border;
66 | }
67 |
68 | html,
69 | body {
70 | @apply bg-background text-foreground;
71 | overscroll-behavior-x: none;
72 | overscroll-behavior-y: none;
73 | }
74 | }
75 |
76 | ::selection {
77 | background-color: rgba(92, 92, 92, 0.471);
78 | color: #fff;
79 | }
80 |
81 | img,
82 | image {
83 | user-select: none;
84 | pointer-events: none;
85 | }
86 |
87 | @media screen and (max-width: 500px) {
88 | body {
89 | font-size: 14px;
90 | }
91 | }
92 |
93 | pre {
94 | padding: 10px;
95 | border-radius: 10px;
96 | }
97 |
98 | .font-subheading {
99 | font-family: "Instrument Serif ", "Instrument Serif Fallback";
100 | font-weight: 500;
101 | }
102 |
103 | @media (max-width: 1024px) {
104 | .chart-container {
105 | width: 120% !important;
106 | transform: translateX(-60px) !important;
107 | }
108 | }
109 |
110 | ::-webkit-scrollbar {
111 | display: none;
112 | }
113 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import "./globals.css";
3 | import Provider from "./provider";
4 | import { TailwindIndicator } from "@/components/globals/tailwind-indicator";
5 | import { Geist, Geist_Mono } from "next/font/google";
6 | import { Toaster } from "@/components/ui/sonner";
7 | import Script from "next/script";
8 |
9 | const geistSans = Geist({
10 | variable: "--font-geist-sans",
11 | subsets: ["latin"],
12 | });
13 |
14 | const geistMono = Geist_Mono({
15 | variable: "--font-geist-mono",
16 | subsets: ["latin"],
17 | });
18 |
19 | export const metadata: Metadata = {
20 | title: "Analytics Dashboard | Real-Time Business Insights",
21 | description:
22 | "Comprehensive analytics dashboard providing real-time business insights, data visualization, performance metrics, and customizable reporting tools.",
23 | keywords:
24 | "analytics dashboard, business intelligence, data visualization, performance metrics, KPI tracking, real-time analytics, business insights, reporting tools, data analysis",
25 | };
26 |
27 | export default function RootLayout({
28 | children,
29 | }: Readonly<{
30 | children: React.ReactNode;
31 | }>) {
32 | return (
33 |
34 |
39 |
42 |
43 | {children}
44 |
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mihir2423/analytics/a648ca4619acd1da85267ad831f7851167779982/src/app/opengraph-image.png
--------------------------------------------------------------------------------
/src/app/provider.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { headers } from "next/headers";
3 | import { SessionProvider } from "next-auth/react";
4 | import { auth } from "@/auth";
5 |
6 | type Props = {
7 | children: React.ReactNode;
8 | };
9 |
10 | const Provider = async ({ children }: Props) => {
11 | // Wait for headers to be available
12 | await headers();
13 |
14 | // Now we can safely call auth()
15 | const session = await auth();
16 |
17 | return {children} ;
18 | };
19 |
20 | export default Provider;
21 |
--------------------------------------------------------------------------------
/src/app/twitter-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mihir2423/analytics/a648ca4619acd1da85267ad831f7851167779982/src/app/twitter-image.png
--------------------------------------------------------------------------------
/src/auth.config.ts:
--------------------------------------------------------------------------------
1 | import Google from "next-auth/providers/google";
2 |
3 | export const authConfig = {
4 | providers: [Google],
5 | };
6 |
--------------------------------------------------------------------------------
/src/auth.ts:
--------------------------------------------------------------------------------
1 | import { authConfig } from "@/auth.config";
2 | import db from "@/lib/db";
3 | import { PrismaAdapter } from "@auth/prisma-adapter";
4 | import type { DefaultSession } from "next-auth";
5 | import NextAuth from "next-auth";
6 |
7 | declare module "next-auth" {
8 | interface Session {
9 | user: DefaultSession["user"] & {
10 | id: string;
11 | role?: string;
12 | };
13 | }
14 | }
15 |
16 | import Google from "next-auth/providers/google";
17 |
18 | declare module "next-auth" {
19 | interface User {
20 | role?: string;
21 | }
22 | }
23 |
24 | export const { handlers, signIn, signOut, auth } = NextAuth({
25 | ...authConfig,
26 | adapter: PrismaAdapter(db),
27 | providers: [
28 | Google({
29 | clientId: process.env.AUTH_GOOGLE_ID || "",
30 | clientSecret: process.env.AUTH_GOOGLE_SECRET || "",
31 | }),
32 | ],
33 | session: {
34 | strategy: "jwt",
35 | },
36 | pages: {
37 | signIn: "/signin",
38 | },
39 | callbacks: {
40 | async jwt({ token, user }) {
41 | if (user) {
42 | // get user from db with the email
43 | // if there is no user with the email, create new user
44 | // else set the user data to token
45 | token.id = user.id;
46 | token.role = user.role || "USER";
47 | }
48 | return token;
49 | },
50 |
51 | async session({ session, token }) {
52 | if (token) {
53 | // set the token data to session
54 | if (session.user) {
55 | session.user.id = token.id as string;
56 | session.user.role = token.role as string;
57 | }
58 | }
59 |
60 | return session;
61 | },
62 |
63 | redirect() {
64 | return "/signin";
65 | },
66 | },
67 | });
68 |
--------------------------------------------------------------------------------
/src/components/globals/animation-container.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion } from "framer-motion";
4 | import { type ReactNode } from "react";
5 | import React from "react";
6 |
7 | interface AnimationContainerProps {
8 | children: ReactNode;
9 | className?: string;
10 | animation?: "fadeUp" | "fadeDown" | "fadeLeft" | "fadeRight" | "scaleUp";
11 | delay?: number;
12 | }
13 |
14 | const getAnimationVariants = (animation: string) => {
15 | switch (animation) {
16 | case "fadeUp":
17 | return { opacity: 0, y: 20 };
18 | case "fadeDown":
19 | return { opacity: 0, y: -20 };
20 | case "fadeLeft":
21 | return { opacity: 0, x: -20 };
22 | case "fadeRight":
23 | return { opacity: 0, x: 20 };
24 | case "scaleUp":
25 | return { opacity: 0, scale: 0.95 };
26 | default:
27 | return { opacity: 0, y: 20 };
28 | }
29 | };
30 |
31 | const AnimationContainer = ({
32 | children,
33 | className,
34 | animation = "fadeUp",
35 | delay = 0,
36 | }: AnimationContainerProps) => {
37 | return (
38 |
54 | {children}
55 |
56 | );
57 | };
58 |
59 | export default AnimationContainer;
60 |
--------------------------------------------------------------------------------
/src/components/globals/component-wrapper.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | type Props = {
4 | children: React.ReactNode;
5 | };
6 |
7 | export const ComponentWrapper = ({ children }: Props) => {
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/globals/flashlight-wrapper.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect, useRef } from "react";
4 |
5 | const FlashlightWrapper: React.FC<{ children: React.ReactNode }> = ({
6 | children,
7 | }) => {
8 | const flashlightRef = useRef(null);
9 |
10 | useEffect(() => {
11 | const handleMouseMove = (e: MouseEvent | TouchEvent) => {
12 | let mouseX, mouseY;
13 |
14 | if (e instanceof TouchEvent) {
15 | mouseX = e.touches[0].pageX;
16 | mouseY = e.touches[0].pageY;
17 | } else {
18 | mouseX = e.pageX;
19 | mouseY = e.pageY;
20 | }
21 |
22 | if (flashlightRef.current) {
23 | flashlightRef.current.style.setProperty("--Xpos", `${mouseX}px`);
24 | flashlightRef.current.style.setProperty("--Ypos", `${mouseY}px`);
25 | }
26 | };
27 |
28 | document.addEventListener("mousemove", handleMouseMove);
29 | document.addEventListener("touchmove", handleMouseMove);
30 |
31 | return () => {
32 | document.removeEventListener("mousemove", handleMouseMove);
33 | document.removeEventListener("touchmove", handleMouseMove);
34 | };
35 | }, []);
36 |
37 | return (
38 |
39 | {children}
40 |
41 | );
42 | };
43 |
44 | export default FlashlightWrapper;
45 |
--------------------------------------------------------------------------------
/src/components/globals/icons.tsx:
--------------------------------------------------------------------------------
1 | import { LucideProps } from "lucide-react";
2 | import React from "react";
3 |
4 | const Icons = {
5 | icon: (props: LucideProps) => (
6 |
14 |
18 |
22 |
23 |
31 |
32 |
33 |
34 |
42 |
43 |
44 |
45 |
46 |
47 | ),
48 | logo: (props: LucideProps) => (
49 |
57 |
61 |
65 |
69 |
70 |
78 |
79 |
80 |
81 |
89 |
90 |
91 |
92 |
100 |
101 |
102 |
103 |
104 |
105 | ),
106 | };
107 |
108 | export default Icons;
109 |
--------------------------------------------------------------------------------
/src/components/globals/motion-tab.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion } from "framer-motion";
4 |
5 | type TabItem = {
6 | id: string;
7 | label: string;
8 | };
9 |
10 | type MotionTabProps = {
11 | tabs: TabItem[];
12 | activeTab: string;
13 | setActiveTab: (tabId: string) => void;
14 | };
15 |
16 | export function MotionTab({ tabs, activeTab, setActiveTab }: MotionTabProps) {
17 | return (
18 |
19 | {tabs.map((tab) => (
20 | setActiveTab(tab.id)}
23 | className={`${
24 | activeTab === tab.id ? "" : "hover:text-white/60"
25 | } relative rounded-[16px] px-3 py-1.5 text-sm font-medium text-white outline-sky-400 transition focus-visible:outline-2`}
26 | style={{
27 | WebkitTapHighlightColor: "transparent",
28 | }}
29 | >
30 | {activeTab === tab.id && (
31 |
37 | )}
38 | {tab.label}
39 |
40 | ))}
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/globals/tailwind-indicator.tsx:
--------------------------------------------------------------------------------
1 | export function TailwindIndicator() {
2 | // Don't show in production
3 | if (process.env.NODE_ENV === "production") return null;
4 | return (
5 |
6 |
xs
7 |
sm
8 |
md
9 |
lg
10 |
xl
11 |
2xl
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/globals/wrapper.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { cn } from "@/lib/utils";
3 |
4 | interface Props {
5 | className?: string;
6 | children: React.ReactNode;
7 | }
8 |
9 | const Wrapper = ({ className, children }: Props) => {
10 | return (
11 |
19 | );
20 | };
21 |
22 | export default Wrapper;
23 |
--------------------------------------------------------------------------------
/src/components/layout/navbar.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
3 | import { Menu } from "lucide-react";
4 | import Image from "next/image";
5 | import Link from "next/link";
6 | import { Sidebar } from "./sidebar";
7 |
8 | export const Navbar = () => {
9 | return (
10 |
11 |
12 |
13 |
14 | Weblytics
15 |
16 |
17 |
18 |
19 |
20 |
24 |
25 | Toggle menu
26 |
27 |
28 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/layout/sidebar-links.tsx:
--------------------------------------------------------------------------------
1 | import { hexToRGBA } from "@/lib/utils";
2 | import Link from "next/link";
3 | import { usePathname } from "next/navigation";
4 |
5 | interface SidebarLinkProps {
6 | href: string;
7 | icon: React.ComponentType<{ size: number; color: string }>;
8 | label: string;
9 | isCollapsed: boolean;
10 | iconColor: string;
11 | pattern: RegExp;
12 | }
13 |
14 | export const SidebarLink = ({
15 | href,
16 | icon: Icon,
17 | label,
18 | isCollapsed,
19 | iconColor,
20 | pattern,
21 | }: SidebarLinkProps) => {
22 | const pathname = usePathname();
23 | const isActive = pattern.test(pathname);
24 |
25 | return (
26 |
27 |
38 |
39 | {!isCollapsed && (
40 |
45 | {label}
46 |
47 | )}
48 |
49 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/src/components/layout/sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
4 | import { Button } from "@/components/ui/button";
5 | import sidebarLinks from "@/config/sidebar";
6 | import { useSidebar } from "@/contexts/sidebar-context";
7 | import { useMediaQuery } from "@/hooks/use-media-query";
8 | import { LogOut, PanelRightClose, PanelRightOpen } from "lucide-react";
9 | import { signOut, useSession } from "next-auth/react";
10 | import Image from "next/image";
11 | import Link from "next/link";
12 | import { SidebarLink } from "./sidebar-links";
13 |
14 | export const Sidebar = () => {
15 | const { isCollapsed, toggleSidebar } = useSidebar();
16 | const isMobile = useMediaQuery("(max-width: 768px)");
17 | const session = useSession();
18 |
19 | return (
20 |
89 | );
90 | };
91 |
--------------------------------------------------------------------------------
/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion";
5 | import { ChevronDown } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Accordion = AccordionPrimitive.Root;
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ));
21 | AccordionItem.displayName = "AccordionItem";
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className,
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ));
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ));
55 | AccordionContent.displayName = AccordionPrimitive.Content.displayName;
56 |
57 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
58 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ));
21 | Avatar.displayName = AvatarPrimitive.Root.displayName;
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ));
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ));
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
49 |
50 | export { Avatar, AvatarImage, AvatarFallback };
51 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | },
35 | );
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean;
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button";
46 | return (
47 |
52 | );
53 | },
54 | );
55 | Button.displayName = "Button";
56 |
57 | export { Button, buttonVariants };
58 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Card.displayName = "Card";
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | CardHeader.displayName = "CardHeader";
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLDivElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ));
42 | CardTitle.displayName = "CardTitle";
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLDivElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ));
54 | CardDescription.displayName = "CardDescription";
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ));
62 | CardContent.displayName = "CardContent";
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ));
74 | CardFooter.displayName = "CardFooter";
75 |
76 | export {
77 | Card,
78 | CardHeader,
79 | CardFooter,
80 | CardTitle,
81 | CardDescription,
82 | CardContent,
83 | };
84 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DialogPrimitive from "@radix-ui/react-dialog";
5 | import { X } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = DialogPrimitive.Portal;
14 |
15 | const DialogClose = DialogPrimitive.Close;
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ));
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ));
54 | DialogContent.displayName = DialogPrimitive.Content.displayName;
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | );
68 | DialogHeader.displayName = "DialogHeader";
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
78 | );
79 | DialogFooter.displayName = "DialogFooter";
80 |
81 | const DialogTitle = React.forwardRef<
82 | React.ElementRef,
83 | React.ComponentPropsWithoutRef
84 | >(({ className, ...props }, ref) => (
85 |
93 | ));
94 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
95 |
96 | const DialogDescription = React.forwardRef<
97 | React.ElementRef,
98 | React.ComponentPropsWithoutRef
99 | >(({ className, ...props }, ref) => (
100 |
105 | ));
106 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
107 |
108 | export {
109 | Dialog,
110 | DialogPortal,
111 | DialogOverlay,
112 | DialogTrigger,
113 | DialogClose,
114 | DialogContent,
115 | DialogHeader,
116 | DialogFooter,
117 | DialogTitle,
118 | DialogDescription,
119 | };
120 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | );
18 | },
19 | );
20 | Input.displayName = "Input";
21 |
22 | export { Input };
23 |
--------------------------------------------------------------------------------
/src/components/ui/marquee.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import React from "react";
3 | import { ComponentPropsWithoutRef } from "react";
4 |
5 | interface MarqueeProps extends ComponentPropsWithoutRef<"div"> {
6 | /**
7 | * Optional CSS class name to apply custom styles
8 | */
9 | className?: string;
10 | /**
11 | * Whether to reverse the animation direction
12 | * @default false
13 | */
14 | reverse?: boolean;
15 | /**
16 | * Whether to pause the animation on hover
17 | * @default false
18 | */
19 | pauseOnHover?: boolean;
20 | /**
21 | * Content to be displayed in the marquee
22 | */
23 | children: React.ReactNode;
24 | /**
25 | * Whether to animate vertically instead of horizontally
26 | * @default false
27 | */
28 | vertical?: boolean;
29 | /**
30 | * Number of times to repeat the content
31 | * @default 4
32 | */
33 | repeat?: number;
34 | }
35 |
36 | export default function Marquee({
37 | className,
38 | reverse = false,
39 | pauseOnHover = false,
40 | children,
41 | vertical = false,
42 | repeat = 4,
43 | ...props
44 | }: MarqueeProps) {
45 | return (
46 |
57 | {Array(repeat)
58 | .fill(0)
59 | .map((_, i) => (
60 |
69 | {children}
70 |
71 | ))}
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/ui/section-badge.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | interface Props {
4 | title: string;
5 | }
6 |
7 | const SectionBadge = ({ title }: Props) => {
8 | return (
9 |
10 |
16 |
17 | {title}
18 |
19 |
20 | );
21 | };
22 |
23 | export default SectionBadge;
24 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SheetPrimitive from "@radix-ui/react-dialog";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 | import { X } from "lucide-react";
7 |
8 | import { cn } from "@/lib/utils";
9 |
10 | const Sheet = SheetPrimitive.Root;
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger;
13 |
14 | const SheetClose = SheetPrimitive.Close;
15 |
16 | const SheetPortal = SheetPrimitive.Portal;
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ));
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
32 |
33 | const sheetVariants = cva(
34 | "z-50 fixed gap-4 bg-background shadow-lg p-6 transition data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 ease-in-out",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | },
50 | );
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 |
68 |
69 | Close
70 |
71 | {children}
72 |
73 |
74 | ));
75 | SheetContent.displayName = SheetPrimitive.Content.displayName;
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | );
89 | SheetHeader.displayName = "SheetHeader";
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | );
103 | SheetFooter.displayName = "SheetFooter";
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ));
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName;
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ));
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName;
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | };
141 |
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | );
13 | }
14 |
15 | export { Skeleton };
16 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "next-themes";
4 | import { Toaster as Sonner } from "sonner";
5 | import { X, Check } from "lucide-react";
6 |
7 | type ToasterProps = React.ComponentProps;
8 |
9 | const Toaster = ({ ...props }: ToasterProps) => {
10 | const { theme = "system" } = useTheme();
11 |
12 | return (
13 |
32 |
33 |
34 | ),
35 | error: (
36 |
37 |
38 |
39 | ),
40 | }}
41 | {...props}
42 | />
43 | );
44 | };
45 |
46 | export { Toaster };
47 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TabsPrimitive from "@radix-ui/react-tabs";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ));
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ));
53 | TabsContent.displayName = TabsPrimitive.Content.displayName;
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent };
56 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<"textarea">
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | );
19 | });
20 | Textarea.displayName = "Textarea";
21 |
22 | export { Textarea };
23 |
--------------------------------------------------------------------------------
/src/config/code.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { codeToHtml } from "shiki";
4 | import React from "react";
5 |
6 | export async function NextJsScript() {
7 | const html = await codeToHtml(nextJsScript, {
8 | lang: "tsx",
9 | theme: "github-dark",
10 | });
11 |
12 | return html;
13 | }
14 |
15 | export function CodeDisplay({ html }: { html: string }) {
16 | return (
17 |
27 | );
28 | }
29 |
30 | export const nextJsScript = `import Script from "next/script";
31 | `;
36 |
37 | export async function ReactJsScript() {
38 | const html = await codeToHtml(reactJsScript, {
39 | lang: "tsx",
40 | theme: "github-dark",
41 | });
42 |
43 | return html;
44 | }
45 |
46 | export const reactJsScript = ``;
48 |
--------------------------------------------------------------------------------
/src/config/faq.ts:
--------------------------------------------------------------------------------
1 | export type FAQItem = {
2 | question: string;
3 | answer: string;
4 | };
5 |
6 | export const FAQS: FAQItem[] = [
7 | {
8 | question: "What kind of analytics does this tool provide?",
9 | answer:
10 | "Our platform provides insights into website metadata, Open Graph images, and essential SEO information to help you understand how your site appears in search engines and social media.",
11 | },
12 | {
13 | question: "Do I need technical knowledge to use this tool?",
14 | answer:
15 | "Not at all! Our user-friendly interface makes it easy for anyone to analyze a website’s SEO performance and metadata without requiring coding or technical expertise.",
16 | },
17 | {
18 | question: "How do I check my website’s SEO data?",
19 | answer:
20 | "Simply enter your website URL, and our tool will fetch and display key SEO data, including title tags, meta descriptions, Open Graph images, and indexing information.",
21 | },
22 | {
23 | question: "Is this tool free to use?",
24 | answer: "Yes, our basic features are completely free.",
25 | },
26 | ];
27 |
--------------------------------------------------------------------------------
/src/config/features.ts:
--------------------------------------------------------------------------------
1 | export interface FeaturesItem {
2 | title: string;
3 | description: string;
4 | icon: string;
5 | }
6 |
7 | export const Features: FeaturesItem[] = [
8 | {
9 | title: "Traffic Overview",
10 | description: "Get a clear breakdown of your website visitors.",
11 | icon: "/icons/perk-one.svg",
12 | },
13 | {
14 | title: "In-Depth Analytics",
15 | description: "Gain actionable insights with detailed reports.",
16 | icon: "/icons/perk-two.svg",
17 | },
18 | {
19 | title: "SEO Insights",
20 | description: "View essential metadata, OG images, and indexing info.",
21 | icon: "/icons/perk-three.svg",
22 | },
23 | {
24 | title: "User Engagement",
25 | description: "Track visitor interactions and conversion rates.",
26 | icon: "/icons/perk-four.svg",
27 | },
28 | ];
29 |
--------------------------------------------------------------------------------
/src/config/nav.ts:
--------------------------------------------------------------------------------
1 | export const NAV_LINKS = [
2 | {
3 | name: "Features",
4 | link: "#",
5 | },
6 | {
7 | name: "Pricing",
8 | link: "#",
9 | },
10 | {
11 | name: "Contact",
12 | link: "#",
13 | },
14 | ];
15 |
--------------------------------------------------------------------------------
/src/config/sidebar.ts:
--------------------------------------------------------------------------------
1 | import { FolderGit2, Settings } from "lucide-react";
2 |
3 | interface SidebarLinkItem {
4 | href: string;
5 | icon: React.ComponentType<{ size: number; color: string }>;
6 | label: string;
7 | iconColor: string;
8 | pattern: RegExp;
9 | }
10 |
11 | const sidebarLinks: SidebarLinkItem[] = [
12 | // {
13 | // href: "/dashboard",
14 | // icon: LayoutDashboard,
15 | // label: "Dashboard",
16 | // iconColor: "#5b98ff",
17 | // pattern: /^\/dashboard/,
18 | // },
19 | {
20 | href: "/projects",
21 | icon: FolderGit2,
22 | label: "Projects",
23 | iconColor: "#fc8c14",
24 | pattern: /^\/projects/,
25 | },
26 | {
27 | href: "/settings",
28 | icon: Settings,
29 | label: "Settings",
30 | iconColor: "#54ffff",
31 | pattern: /^\/settings/,
32 | },
33 | ];
34 |
35 | export default sidebarLinks;
36 |
--------------------------------------------------------------------------------
/src/contexts/project-context.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { createContext, useContext } from "react";
4 |
5 | type ProjectContextType = {
6 | favIcon: string;
7 | setFavIcon: (favIcon: string) => void;
8 | };
9 |
10 | const ProjectContext = createContext(undefined);
11 |
12 | export const ProjectProvider = ({
13 | children,
14 | }: {
15 | children: React.ReactNode;
16 | }) => {
17 | const [favIcon, setFavIconState] = React.useState("");
18 |
19 | const setFavIcon = (favIcon: string) => {
20 | setFavIconState(favIcon);
21 | };
22 |
23 | return (
24 |
25 | {children}
26 |
27 | );
28 | };
29 |
30 | export const useProject = () => {
31 | const context = useContext(ProjectContext);
32 | if (context === undefined) {
33 | throw new Error("useProject must be used within a ProjectProvider");
34 | }
35 | return context;
36 | };
37 |
--------------------------------------------------------------------------------
/src/contexts/sidebar-context.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | createContext,
5 | useCallback,
6 | useContext,
7 | useEffect,
8 | useState,
9 | } from "react";
10 |
11 | type SidebarContextType = {
12 | isCollapsed: boolean;
13 | toggleSidebar: () => void;
14 | };
15 |
16 | const SidebarContext = createContext(undefined);
17 |
18 | export const SidebarProvider = ({
19 | children,
20 | }: {
21 | children: React.ReactNode;
22 | }) => {
23 | const [isCollapsed, setIsCollapsed] = useState(false);
24 |
25 | const toggleSidebar = useCallback(() => {
26 | setIsCollapsed((prev) => !prev);
27 | }, []);
28 |
29 | useEffect(() => {
30 | const handleKeyDown = (event: KeyboardEvent) => {
31 | if (
32 | (event.metaKey && event.key === "b") ||
33 | (event.ctrlKey && event.key === "b")
34 | ) {
35 | event.preventDefault();
36 | toggleSidebar();
37 | }
38 | };
39 |
40 | window.addEventListener("keydown", handleKeyDown);
41 |
42 | return () => {
43 | window.removeEventListener("keydown", handleKeyDown);
44 | };
45 | }, [toggleSidebar]);
46 |
47 | return (
48 |
49 | {children}
50 |
51 | );
52 | };
53 |
54 | export const useSidebar = () => {
55 | const context = useContext(SidebarContext);
56 | if (context === undefined) {
57 | throw new Error("useSidebar must be used within a SidebarProvider");
58 | }
59 | return context;
60 | };
61 |
--------------------------------------------------------------------------------
/src/data-access/projects.ts:
--------------------------------------------------------------------------------
1 | import "server-only";
2 | import prisma from "@/lib/db";
3 |
4 | export const getProjects = async (userId: string) => {
5 | const res = await prisma.project.findMany({
6 | where: {
7 | ownerId: userId,
8 | },
9 | });
10 | return res;
11 | };
12 |
13 | export const getDomainProject = async (domain: string) => {
14 | const res = await prisma.project.findFirst({
15 | where: {
16 | domain,
17 | },
18 | });
19 | return res;
20 | };
21 |
22 | export const getDomainAnalytics = async (domain: string) => {
23 | const res = await prisma.project.findFirst({
24 | where: {
25 | domain,
26 | },
27 | include: {
28 | analytics: {
29 | include: {
30 | visitHistory: true,
31 | routeAnalytics: true,
32 | countryAnalytics: true,
33 | deviceAnalytics: true,
34 | osAnalytics: true,
35 | sourceAnalytics: true,
36 | },
37 | },
38 | },
39 | });
40 | return res;
41 | };
42 |
43 | export const getLogs = async () => {
44 | const logs = await prisma.log.findMany({
45 | orderBy: {
46 | createdAt: "desc",
47 | },
48 | });
49 | return logs;
50 | };
51 |
--------------------------------------------------------------------------------
/src/hooks/use-click-outside.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 |
3 | export const useClickOutside = (callback: () => void) => {
4 | const ref = useRef(null);
5 |
6 | useEffect(() => {
7 | const handleClickOutside = (event: MouseEvent) => {
8 | if (ref.current && !ref.current.contains(event.target as Node)) {
9 | callback();
10 | }
11 | };
12 |
13 | document.addEventListener("mousedown", handleClickOutside);
14 | return () => {
15 | document.removeEventListener("mousedown", handleClickOutside);
16 | };
17 | }, [callback]);
18 |
19 | return ref;
20 | };
21 |
--------------------------------------------------------------------------------
/src/hooks/use-media-query.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useEffect } from "react";
4 |
5 | export function useMediaQuery(query: string): boolean {
6 | const [matches, setMatches] = useState(false);
7 |
8 | useEffect(() => {
9 | const media = window.matchMedia(query);
10 | if (media.matches !== matches) {
11 | setMatches(media.matches);
12 | }
13 | const listener = () => setMatches(media.matches);
14 | media.addListener(listener);
15 | return () => media.removeListener(listener);
16 | }, [matches, query]);
17 |
18 | return matches;
19 | }
20 |
--------------------------------------------------------------------------------
/src/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | const prismaClientSingleton = () => {
4 | return new PrismaClient();
5 | };
6 |
7 | declare const globalThis: {
8 | prismaGlobal: ReturnType;
9 | } & typeof global;
10 |
11 | const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();
12 |
13 | export default prisma;
14 |
15 | if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma;
16 |
--------------------------------------------------------------------------------
/src/lib/metadata.ts:
--------------------------------------------------------------------------------
1 | // lib/metadata.js
2 | import * as cheerio from "cheerio";
3 | import fetch from "node-fetch";
4 |
5 | export async function extractMetadata(url: string) {
6 | try {
7 | // Fetch the HTML content
8 | const response = await fetch(url, {
9 | headers: {
10 | "User-Agent":
11 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
12 | },
13 | });
14 |
15 | if (!response.ok) {
16 | throw new Error(
17 | `Failed to fetch URL: ${response.status} ${response.statusText}`,
18 | );
19 | }
20 |
21 | const html = await response.text();
22 |
23 | // Parse HTML with cheerio
24 | const $ = cheerio.load(html);
25 |
26 | // Extract metadata
27 | const data = {
28 | title:
29 | $("title").text() ||
30 | $('meta[property="og:title"]').attr("content") ||
31 | null,
32 | description:
33 | $('meta[name="description"]').attr("content") ||
34 | $('meta[property="og:description"]').attr("content") ||
35 | null,
36 | image: $('meta[property="og:image"]').attr("content") || null,
37 | url: $('meta[property="og:url"]').attr("content") || url,
38 | favicon: $('link[rel="icon"]').attr("href") || null,
39 | };
40 |
41 | return { data, error: null };
42 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
43 | } catch (error: any) {
44 | console.error("Error extracting metadata:", error);
45 | return { data: null, error: error.message };
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/lib/safe-action.ts:
--------------------------------------------------------------------------------
1 | import { assertAuthenticated } from "./session";
2 | import { createServerActionProcedure } from "zsa";
3 | import { PublicError } from "./utils";
4 |
5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
6 | function shapeErrors({ err }: any) {
7 | const isAllowedError = err instanceof PublicError;
8 | const isDev = process.env.NODE_ENV === "development";
9 | if (isDev && !isAllowedError) {
10 | console.error(err);
11 | return {
12 | code: err.code ?? "Error",
13 | message: `${!isAllowedError && isDev ? "DEV ONLY ENABLED - " : ""}${
14 | err.message
15 | }`,
16 | };
17 | } else {
18 | return {
19 | code: "Error",
20 | message: "Something went wrong!",
21 | };
22 | }
23 | }
24 |
25 | export const authenticatedAction = createServerActionProcedure()
26 | .experimental_shapeError(shapeErrors)
27 | .handler(async () => {
28 | const user = await assertAuthenticated();
29 | return { user };
30 | });
31 |
32 | export const unauthenticatedAction = createServerActionProcedure()
33 | .experimental_shapeError(shapeErrors)
34 | .handler(async () => {});
35 |
--------------------------------------------------------------------------------
/src/lib/session.ts:
--------------------------------------------------------------------------------
1 | import "server-only";
2 |
3 | import { auth } from "@/auth";
4 | import { AuthenticationError } from "./utils";
5 |
6 | export const getCurrentUser = async () => {
7 | const session = await auth();
8 | if (!session || !session.user) {
9 | return undefined;
10 | }
11 | return session.user;
12 | };
13 |
14 | export const assertAuthenticated = async () => {
15 | const user = await getCurrentUser();
16 | if (!user) {
17 | throw new AuthenticationError();
18 | }
19 | return user;
20 | };
21 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
8 | export const hexToRGBA = (hex: string, opacity: number): string => {
9 | hex = hex.replace("#", "");
10 | const r = parseInt(hex.substring(0, 2), 16);
11 | const g = parseInt(hex.substring(2, 4), 16);
12 | const b = parseInt(hex.substring(4, 6), 16);
13 | return `rgba(${r}, ${g}, ${b}, ${opacity})`;
14 | };
15 |
16 | export const AUTHENTICATION_ERROR_MESSAGE =
17 | "You must be logged in to view this content";
18 |
19 | export const AuthenticationError = class AuthenticationError extends Error {
20 | constructor() {
21 | super(AUTHENTICATION_ERROR_MESSAGE);
22 | this.name = "AuthenticationError";
23 | }
24 | };
25 |
26 | export class PublicError extends Error {
27 | constructor(message: string) {
28 | super(message);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_LOGIN_REDIRECT, PUBLIC_ROUTES, AUTH_ROUTES } from "@/routes";
2 | import { NextResponse } from "next/server";
3 | import { auth } from "@/auth";
4 |
5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
6 | export default auth((req: any) => {
7 | const { nextUrl } = req;
8 |
9 | const isAuthenticated = !!req.auth;
10 |
11 | if (
12 | nextUrl.pathname === "/api/track" ||
13 | nextUrl.pathname === "/tracking-script.js"
14 | ) {
15 | console.log("Passed");
16 | return NextResponse.next();
17 | }
18 |
19 | console.log("isAuthenticated", isAuthenticated);
20 |
21 | const isAuthRoute = AUTH_ROUTES.includes(nextUrl.pathname);
22 | if (
23 | nextUrl.pathname.startsWith("/api") ||
24 | PUBLIC_ROUTES.includes(nextUrl.pathname)
25 | ) {
26 | return NextResponse.next(); // Allow the request to proceed
27 | }
28 |
29 | if (isAuthRoute && isAuthenticated)
30 | return NextResponse.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl));
31 |
32 | if (!isAuthenticated && !isAuthRoute)
33 | return NextResponse.redirect(new URL("/signin", nextUrl));
34 | });
35 |
36 | export const config = {
37 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
38 | };
39 |
--------------------------------------------------------------------------------
/src/routes.ts:
--------------------------------------------------------------------------------
1 | /*
2 | @type {string[]}
3 | */
4 | export const AUTH_ROUTES = ["/signin"];
5 | export const PUBLIC_ROUTES = ["/"];
6 | export const DEFAULT_LOGIN_REDIRECT = "/projects";
7 |
--------------------------------------------------------------------------------
/src/store/store.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import { useEffect } from "react";
3 | import { create } from "zustand";
4 |
5 | type ModalType =
6 | | "createProject"
7 | | "editProject"
8 | | "deleteProject"
9 | | "createBugReport"
10 | | null;
11 |
12 | interface ModalStore {
13 | isOpen: boolean;
14 | type: ModalType;
15 | data?: any;
16 | onOpen: (type: ModalType, data?: any) => void;
17 | onClose: () => void;
18 | }
19 |
20 | export const useModal = create((set) => ({
21 | isOpen: false,
22 | type: null,
23 | data: undefined,
24 | onOpen: (type, data) => set({ isOpen: true, type, data }),
25 | onClose: () => set({ isOpen: false, type: null, data: undefined }),
26 | }));
27 |
28 | interface TabStore {
29 | activeTab: string;
30 | setActiveTab: (tabId: string) => void;
31 | }
32 |
33 | export const useTabStore = create((set) => ({
34 | activeTab: "metadata",
35 | setActiveTab: (tabId) => set({ activeTab: tabId }),
36 | }));
37 |
38 | export const useSettingsTabStore = create((set) => ({
39 | activeTab: "logs",
40 | setActiveTab: (tabId) => set({ activeTab: tabId }),
41 | }));
42 |
43 | export const useKeyboardShortcut = () => {
44 | const { onOpen } = useModal();
45 |
46 | useEffect(() => {
47 | const handleKeyDown = (event: KeyboardEvent) => {
48 | if ((event.metaKey || event.ctrlKey) && event.key === "m") {
49 | event.preventDefault(); // Prevent the default behavior of the browser
50 | onOpen("createProject");
51 | }
52 | };
53 |
54 | window.addEventListener("keydown", handleKeyDown);
55 |
56 | return () => {
57 | window.removeEventListener("keydown", handleKeyDown);
58 | };
59 | }, [onOpen]);
60 | };
61 |
--------------------------------------------------------------------------------
/src/types/index.d.ts:
--------------------------------------------------------------------------------
1 | declare type Project = {
2 | id: string;
3 | domain: string;
4 | name: string;
5 | description: string | null;
6 | ownerId: string;
7 | createdAt: Date;
8 | updatedAt: Date;
9 | };
10 |
--------------------------------------------------------------------------------
/src/use-cases/projects.ts:
--------------------------------------------------------------------------------
1 | import "server-only";
2 | import {
3 | getDomainAnalytics,
4 | getDomainProject,
5 | getLogs,
6 | getProjects,
7 | } from "@/data-access/projects";
8 |
9 | import { unstable_cache as cache } from "next/cache";
10 |
11 | export const getAllProjects = async (id: string | undefined) => {
12 | if (!id) {
13 | return null;
14 | }
15 | try {
16 | const res = await cache(
17 | async () => {
18 | const projects = await getProjects(id);
19 | return projects || [];
20 | },
21 | ["all-projects", id],
22 | { tags: ["projects"] },
23 | )();
24 | return res;
25 | } catch (error) {
26 | console.error("Error fetching all projects:", error);
27 | return [];
28 | }
29 | };
30 | export const getProjectByDomain = async (domain: string | null) => {
31 | if (!domain) {
32 | return null;
33 | }
34 | try {
35 | const res = await getDomainProject(domain);
36 | return res;
37 | } catch (error) {
38 | console.error(`Error fetching project for domain ${domain}:`, error);
39 | return null;
40 | }
41 | };
42 |
43 | export const getAnalytics = async (domain: string | null) => {
44 | if (!domain) {
45 | return null;
46 | }
47 | try {
48 | const res = await getDomainAnalytics(domain);
49 | return res;
50 | } catch (error) {
51 | console.error(`Error fetching analytics for domain ${domain}:`, error);
52 | return null;
53 | }
54 | };
55 |
56 | export const getAllLogs = async () => {
57 | try {
58 | const res = await cache(
59 | async () => {
60 | const logs = await getLogs();
61 | return logs || [];
62 | },
63 | ["all-logs"],
64 | { tags: ["logs"] },
65 | )();
66 | return res;
67 | } catch (error) {
68 | console.error("Error fetching all logs:", error);
69 | return [];
70 | }
71 | };
72 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./app/**/*.{ts,tsx}",
9 | "./src/**/*.{ts,tsx}",
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | background: "hsl(var(--background))",
23 | foreground: "hsl(var(--foreground))",
24 | card: {
25 | DEFAULT: "hsl(var(--card))",
26 | foreground: "hsl(var(--card-foreground))",
27 | },
28 | popover: {
29 | DEFAULT: "hsl(var(--popover))",
30 | foreground: "hsl(var(--popover-foreground))",
31 | },
32 | primary: {
33 | DEFAULT: "hsl(var(--primary))",
34 | foreground: "hsl(var(--primary-foreground))",
35 | },
36 | secondary: {
37 | DEFAULT: "hsl(var(--secondary))",
38 | foreground: "hsl(var(--secondary-foreground))",
39 | },
40 | muted: {
41 | DEFAULT: "hsl(var(--muted))",
42 | foreground: "hsl(var(--muted-foreground))",
43 | },
44 | accent: {
45 | DEFAULT: "hsl(var(--accent))",
46 | foreground: "hsl(var(--accent-foreground))",
47 | },
48 | destructive: {
49 | DEFAULT: "hsl(var(--destructive))",
50 | foreground: "hsl(var(--destructive-foreground))",
51 | },
52 | border: "hsl(var(--border))",
53 | input: "hsl(var(--input))",
54 | ring: "hsl(var(--ring))",
55 | chart: {
56 | "1": "hsl(var(--chart-1))",
57 | "2": "hsl(var(--chart-2))",
58 | "3": "hsl(var(--chart-3))",
59 | "4": "hsl(var(--chart-4))",
60 | "5": "hsl(var(--chart-5))",
61 | },
62 | sidebar: {
63 | DEFAULT: "hsl(var(--sidebar-background))",
64 | foreground: "hsl(var(--sidebar-foreground))",
65 | primary: "hsl(var(--sidebar-primary))",
66 | "primary-foreground": "hsl(var(--sidebar-primary-foreground))",
67 | accent: "hsl(var(--sidebar-accent))",
68 | "accent-foreground": "hsl(var(--sidebar-accent-foreground))",
69 | border: "hsl(var(--sidebar-border))",
70 | ring: "hsl(var(--sidebar-ring))",
71 | },
72 | },
73 | borderRadius: {
74 | lg: "var(--radius)",
75 | md: "calc(var(--radius) - 2px)",
76 | sm: "calc(var(--radius) - 4px)",
77 | },
78 | keyframes: {
79 | "accordion-down": {
80 | from: {
81 | height: "0",
82 | },
83 | to: {
84 | height: "var(--radix-accordion-content-height)",
85 | },
86 | },
87 | "accordion-up": {
88 | from: {
89 | height: "var(--radix-accordion-content-height)",
90 | },
91 | to: {
92 | height: "0",
93 | },
94 | },
95 | marquee: {
96 | from: {
97 | transform: "translateX(0)",
98 | },
99 | to: {
100 | transform: "translateX(calc(-100% - var(--gap)))",
101 | },
102 | },
103 | "marquee-vertical": {
104 | from: {
105 | transform: "translateY(0)",
106 | },
107 | to: {
108 | transform: "translateY(calc(-100% - var(--gap)))",
109 | },
110 | },
111 | },
112 | animation: {
113 | "accordion-down": "accordion-down 0.2s ease-out",
114 | "accordion-up": "accordion-up 0.2s ease-out",
115 | marquee: "marquee var(--duration) linear infinite",
116 | "marquee-vertical": "marquee-vertical var(--duration) linear infinite",
117 | },
118 | spacing: {
119 | "1/8": "12.5%",
120 | },
121 | },
122 | },
123 | // eslint-disable-next-line @typescript-eslint/no-require-imports
124 | plugins: [require("tailwindcss-animate")],
125 | } satisfies Config;
126 |
127 | export default config;
128 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------