├── .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 | project 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 | 19 |

Please sign in to continue

20 |
{ 22 | await signIn("google"); 23 | }} 24 | > 25 | 54 |
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 | {item.label} 78 | 79 | {item.label} 80 | 81 |
82 |
83 | ))} 84 |
85 |
86 |
87 | 88 | 89 | 90 | 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 |
36 | 37 | Plus 44 | 45 |
46 | 47 |
48 | {Features.map((feature, index) => ( 49 |
56 | 60 |
61 |
62 | {feature.title} 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 | 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 |
85 |
86 |
87 |
88 | hero 96 |
97 |
98 |
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 | Logo 27 | 28 | Weblytics 29 | 30 | 31 | 32 | 33 | 34 |
35 | 40 | 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 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 |
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 |
44 | 51 |
52 | 53 | 54 | 55 | 56 |
57 |
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 | 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 | project 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 | 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 | OG Image 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 | 61 | 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 |
27 |
28 |
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 | 57 | 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 | favicon 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 | 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 |
23 | 24 | 25 | 26 | 27 |
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 | 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 |