├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── components.json ├── global.d.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prisma ├── migrations │ ├── 20240628163003_init │ │ └── migration.sql │ ├── 20240704105613_add_org_and_pr_schema │ │ └── migration.sql │ ├── 20240706190459_added_pr_org_rel │ │ └── migration.sql │ ├── 20240709132951_username_made_unique │ │ └── migration.sql │ ├── 20240715160927_bounty_feild_type_updated │ │ └── migration.sql │ ├── 20240718162529_add_new_user_fields │ │ └── migration.sql │ ├── 20240729074221_link_user_and_organisation │ │ └── migration.sql │ ├── 20240930123222_saved_by_field_added_bw_user_and_org │ │ └── migration.sql │ ├── 20241014104957_added_profile_views_schema │ │ └── migration.sql │ ├── 20241014123222_updated_few_feild_in_profile_view │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── custom-cursor.png ├── dashboard.png ├── desktop-image.png ├── next.svg └── vercel.svg ├── src ├── app │ ├── actions │ │ ├── fetchPRs.ts │ │ └── userAction.ts │ ├── api │ │ ├── analytics │ │ │ └── route.ts │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── leaderboard │ │ │ └── route.ts │ │ ├── og-images │ │ │ ├── public-profile │ │ │ │ └── route.tsx │ │ │ └── root │ │ │ │ └── route.tsx │ │ ├── organisation │ │ │ └── route.ts │ │ ├── pr │ │ │ ├── [username] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ └── user │ │ │ └── route.ts │ ├── assets │ │ ├── github.svg │ │ ├── google.svg │ │ └── x.svg │ ├── auth │ │ └── page.tsx │ ├── favicon.ico │ ├── fonts │ │ ├── CircularStd-Bold.ttf │ │ ├── CircularStd-Book.ttf │ │ ├── CircularStd-Medium.ttf │ │ └── main-font.ttf │ ├── globals.css │ ├── hooks │ │ ├── useDebounce.ts │ │ ├── useUserData.tsx │ │ └── useWebsocket.ts │ ├── layout.tsx │ ├── page.tsx │ ├── privacy-policy │ │ └── page.tsx │ ├── profile │ │ └── [slug] │ │ │ └── page.tsx │ ├── refund-policy │ │ └── page.tsx │ ├── terms-and-conditions │ │ └── page.tsx │ ├── types │ │ └── global.d.ts │ └── work │ │ ├── dashboard │ │ └── page.tsx │ │ ├── embed │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── leaderboard │ │ └── page.tsx │ │ ├── my-pr │ │ └── page.tsx │ │ ├── organisations │ │ └── page.tsx │ │ └── profile │ │ └── page.tsx ├── components │ ├── AuxiliaryProvider.tsx │ ├── ContributedOrg.tsx │ ├── Hero.tsx │ ├── LaptopScreen.tsx │ ├── LeaderboardCard.tsx │ ├── Navbar.tsx │ ├── OrgCard.tsx │ ├── PRCard.tsx │ ├── PRListings.tsx │ ├── PersonalDetailsForm.tsx │ ├── ProfileEditForm.tsx │ ├── ProfileSwitch.tsx │ ├── Providers.tsx │ ├── RequestAccessButton.tsx │ ├── Sidebar.tsx │ ├── SignIn.tsx │ ├── TopLeaderCard.tsx │ ├── Topbar.tsx │ ├── WallOfLove.tsx │ ├── alert-box.tsx │ ├── code-block.tsx │ ├── desktop-screen.tsx │ ├── features.tsx │ ├── fixed-marketing-navbar.tsx │ ├── footer.tsx │ ├── frameworks-list.tsx │ ├── google-analytics.tsx │ ├── hero-buttons.tsx │ ├── home-button.tsx │ ├── how-it-works.tsx │ ├── pr-delete-button.tsx │ ├── preview-widget.tsx │ ├── product-demo.tsx │ ├── profile-view.tsx │ ├── profile-views-chart.tsx │ ├── statistics.tsx │ ├── svgs │ │ ├── html.tsx │ │ ├── next-js.tsx │ │ ├── react-js.tsx │ │ └── x.tsx │ ├── testimonials.tsx │ ├── theme-provider.tsx │ ├── theme-toggler.tsx │ ├── title-card.tsx │ ├── ui │ │ ├── avatar-circle.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── chart.tsx │ │ ├── dialog.tsx │ │ ├── input.tsx │ │ ├── safari.tsx │ │ ├── select.tsx │ │ ├── sheet.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── tabs.tsx │ │ └── tooltip.tsx │ ├── users-avatars.tsx │ └── your-profile-button.tsx ├── data │ └── data.ts ├── lib │ ├── auth.ts │ ├── db.ts │ ├── profile.ts │ └── utils.ts ├── middleware.ts ├── store │ └── sidebar.ts └── util │ ├── cn.ts │ ├── index.ts │ └── types.ts ├── tailwind.config.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="" 2 | 3 | GOOGLE_CLIENT_ID = "" 4 | GOOGLE_CLIENT_SECRET = "" 5 | 6 | GITHUB_CLIENT_ID = "" 7 | GITHUB_CLIENT_SECRET = "" 8 | 9 | NEXTAUTH_SECRET = "" 10 | NEXTAUTH_URL = "http://localhost:3000" 11 | 12 | NEXT_PUBLIC_GITHUB_AUTH_TOKEN = "" 13 | 14 | NEXT_PUBLIC_URL = "http://localhost:3000" 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .next 2 | next-env.d.ts 3 | node_modules 4 | yarn.lock 5 | package-lock.json 6 | public 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | #env 10 | .env 11 | 12 | # testing 13 | /coverage 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | *.pem 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | next-env.d.ts 3 | node_modules 4 | yarn.lock 5 | package-lock.json 6 | public 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 80, 6 | "tabWidth": 2, 7 | "plugins": ["prettier-plugin-tailwindcss"] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "editorconfig.editorconfig" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.formatOnPaste": true, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": "always", 7 | "source.fixAll.format": "always" 8 | }, 9 | "prettier.requireConfig": true 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20.12.0-alpine3.19 2 | 3 | WORKDIR /src/app 4 | 5 | COPY . . 6 | 7 | ENV DATABASE_URL = ${DATABASE_URL} 8 | 9 | # Install dependencies 10 | RUN npm install 11 | # Can you add a script to the global package.json that does this? 12 | RUN npx prisma generate 13 | 14 | # Can you filter the build down to just one app? 15 | RUN npm run build 16 | 17 | EXPOSE 3000 18 | 19 | CMD ["npm", "run", "dev"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Aman Kumar Bairagi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Merged&Share 2 | 3 | [![TypeScript](https://img.shields.io/badge/Made%20with-TypeScript-blue.svg)](https://www.typescriptlang.org/) 4 | [![Docker](https://img.shields.io/badge/Docker-configured-blue)](https://www.docker.com/) 5 | [![Next.js](https://img.shields.io/badge/Next.js-14%2B-black?logo=next.js)](https://nextjs.org/) 6 | [![Prisma](https://img.shields.io/badge/Prisma-ORM-pink)](https://www.prisma.io/) 7 | [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-Database-blue)](https://www.postgresql.org/) 8 | 9 | Link: https://mergedandshare.in/ 10 | 11 | Merged&Share is a platform that allows users to fetch their merged pull requests (PRs) from any public GitHub organization. It lets users showcase and share their open-source contributions globally as proof of work. 12 | 13 | ## Features 14 | 15 | - Fetch merged PRs from public GitHub repositories. 16 | - Display and share contributions. 17 | - User-friendly interface. 18 | 19 | ## Tech Stack 20 | 21 | - Next.js 14+ 22 | - TypeScript 23 | - PostgreSQL 24 | - Prisma ORM 25 | - Tailwind CSS 26 | - NextAuth.js 27 | - Zustand (State Management) 28 | - Shadcn-UI 29 | 30 | 31 | ## Prerequisites 32 | 33 | - Node.js (v18 or higher) 34 | - PostgreSQL (running with correct connection details) 35 | - npm (or yarn) 36 | 37 | 38 | ## Installation & Setup 39 | 40 | 1. **Clone the repository:** 41 | 42 | ```bash 43 | git clone https://github.com/amanbairagi30/merged-n-share.git 44 | cd merged-n-share 45 | ``` 46 | 47 | 2. **Install dependencies:** 48 | 49 | ```bash 50 | npm install 51 | ``` 52 | 53 | 3. **Set up environment variables:** 54 | 55 | - Copy `.env.example` to `.env` 56 | - Fill in the required environment variables. See **Environment Variables** section below. 57 | 58 | 4. **Run database migrations:** 59 | 60 | ```bash 61 | npx prisma migrate dev 62 | ``` 63 | 64 | ## Available Scripts 65 | 66 | Currently, the `package.json` does not define any custom scripts. The following standard npm scripts are available: 67 | 68 | - `npm run dev`: Starts the Next.js development server. 69 | - `npm run build`: Builds the application for production. 70 | - `npm run start`: Starts the production server. 71 | 72 | 73 | ## Environment Variables 74 | 75 | Create a `.env` file based on `.env.example` and populate the following variables: 76 | 77 | - `DATABASE_URL`: Your PostgreSQL connection string. Example: `postgresql://user:password@host:port/database` 78 | - `NEXTAUTH_SECRET`: A secret used to encrypt session data. Generate a random string. 79 | - `GITHUB_ID`: Your GitHub application's Client ID. 80 | - `GITHUB_SECRET`: Your GitHub application's Client Secret. 81 | - Add any other environment variables required by your application here. Clearly document their purpose. 82 | 83 | 84 | ## Development Guide 85 | 86 | After completing the installation and setup, run the development server: 87 | 88 | ```bash 89 | npm run dev 90 | ``` 91 | 92 | The application will be available at [http://localhost:3000](http://localhost:3000). 93 | 94 | ## Deployment Instructions 95 | 96 | This project is Docker configured. While the specific Docker commands are not included in the project yet, you will likely need to build a Docker image and then run it. Add detailed Docker instructions here once finalized. Example: 97 | 98 | ```bash 99 | # Build the Docker image 100 | docker build -t merged-and-share . 101 | 102 | # Run the Docker container 103 | docker run -p 3000:3000 merged-and-share 104 | ``` 105 | 106 | Further deployment details (e.g., for specific cloud providers) should be added here as needed. 107 | 108 | 109 | ## Contributing 110 | 111 | Contributions are welcome! Feel free to open issues or submit pull requests. Please ensure your code follows the existing style and include tests for any new features. 112 | 113 | 114 | ## License 115 | 116 | This project is licensed under the MIT License. 117 | -------------------------------------------------------------------------------- /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": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace JSX { 3 | interface IntrinsicElements { 4 | 'widget-web-component': React.DetailedHTMLProps< 5 | React.HTMLAttributes, 6 | HTMLElement 7 | > & { 8 | username: string; 9 | theme: string; 10 | }; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: [ 5 | 'images.unsplash.com', 6 | 'img.icons8.com', 7 | 'appx-wsb.classx.co.in', 8 | 'd2szwvl7yo497w.cloudfront.net', 9 | 'i.pinimg.com', 10 | 'avatars.githubusercontent.com', 11 | ], 12 | }, 13 | async headers() { 14 | return [ 15 | { 16 | // Routes this applies to 17 | source: '/api/(.*)', 18 | // Headers 19 | headers: [ 20 | // Allow for specific domains to have access or * for all 21 | { 22 | key: 'Access-Control-Allow-Origin', 23 | value: '*', 24 | // DOES NOT WORK 25 | // value: process.env.ALLOWED_ORIGIN, 26 | }, 27 | // Allows for specific methods accepted 28 | { 29 | key: 'Access-Control-Allow-Methods', 30 | value: 'GET, POST, PUT, DELETE, OPTIONS', 31 | }, 32 | // Allows for specific headers accepted (These are a few standard ones) 33 | { 34 | key: 'Access-Control-Allow-Headers', 35 | value: 'Content-Type, Authorization', 36 | }, 37 | ], 38 | }, 39 | ]; 40 | }, 41 | }; 42 | 43 | export default nextConfig; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "merged-n-share", 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 | }, 11 | "dependencies": { 12 | "@auth/prisma-adapter": "^2.4.1", 13 | "@prisma/client": "^5.16.1", 14 | "@radix-ui/react-avatar": "^1.1.1", 15 | "@radix-ui/react-dialog": "^1.1.2", 16 | "@radix-ui/react-icons": "^1.3.0", 17 | "@radix-ui/react-select": "^2.1.1", 18 | "@radix-ui/react-slot": "^1.1.0", 19 | "@radix-ui/react-switch": "^1.1.0", 20 | "@radix-ui/react-tabs": "^1.1.1", 21 | "@radix-ui/react-toast": "^1.2.1", 22 | "@radix-ui/react-tooltip": "^1.1.2", 23 | "@types/react-syntax-highlighter": "^15.5.13", 24 | "@vercel/analytics": "^1.3.1", 25 | "class-variance-authority": "^0.7.0", 26 | "clsx": "^2.1.1", 27 | "framer-motion": "^11.2.12", 28 | "lodash": "^4.17.21", 29 | "lucide-react": "^0.399.0", 30 | "mini-svg-data-uri": "^1.4.4", 31 | "next": "14.2.4", 32 | "next-auth": "^4.24.7", 33 | "next-themes": "^0.3.0", 34 | "nextjs-toploader": "^1.6.12", 35 | "react": "^18", 36 | "react-dom": "^18", 37 | "react-syntax-highlighter": "^15.5.0", 38 | "recharts": "^2.13.0", 39 | "sonner": "^1.5.0", 40 | "tailwind-merge": "^2.3.0", 41 | "tailwindcss-animate": "^1.0.7", 42 | "zustand": "^4.5.4" 43 | }, 44 | "devDependencies": { 45 | "@types/lodash": "^4.17.6", 46 | "@types/node": "^20", 47 | "@types/react": "^18", 48 | "@types/react-dom": "^18", 49 | "eslint": "^8", 50 | "eslint-config-next": "14.2.4", 51 | "postcss": "^8", 52 | "prettier": "^3.3.3", 53 | "prettier-plugin-tailwindcss": "^0.6.8", 54 | "prisma": "^5.16.1", 55 | "tailwindcss": "^3.4.1", 56 | "typescript": "^5" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /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/migrations/20240628163003_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Account" ( 3 | "id" TEXT NOT NULL, 4 | "userId" TEXT NOT NULL, 5 | "type" TEXT NOT NULL, 6 | "provider" TEXT NOT NULL, 7 | "providerAccountId" TEXT NOT NULL, 8 | "refresh_token" TEXT, 9 | "refresh_token_expires_in" INTEGER, 10 | "access_token" TEXT, 11 | "expires_at" INTEGER, 12 | "token_type" TEXT, 13 | "scope" TEXT, 14 | "id_token" TEXT, 15 | "session_state" TEXT, 16 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | "updatedAt" TIMESTAMP(3) NOT NULL, 18 | 19 | CONSTRAINT "Account_pkey" PRIMARY KEY ("id") 20 | ); 21 | 22 | -- CreateTable 23 | CREATE TABLE "Session" ( 24 | "id" TEXT NOT NULL, 25 | "sessionToken" TEXT NOT NULL, 26 | "userId" TEXT NOT NULL, 27 | "expires" TIMESTAMP(3) NOT NULL, 28 | 29 | CONSTRAINT "Session_pkey" PRIMARY KEY ("id") 30 | ); 31 | 32 | -- CreateTable 33 | CREATE TABLE "User" ( 34 | "id" TEXT NOT NULL, 35 | "name" TEXT, 36 | "username" TEXT, 37 | "email" TEXT, 38 | "emailVerified" TIMESTAMP(3), 39 | "image" TEXT, 40 | "admin" BOOLEAN NOT NULL DEFAULT false, 41 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 42 | "updatedAt" TIMESTAMP(3) NOT NULL, 43 | 44 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 45 | ); 46 | 47 | -- CreateTable 48 | CREATE TABLE "VerificationToken" ( 49 | "identifier" TEXT NOT NULL, 50 | "token" TEXT NOT NULL, 51 | "expires" TIMESTAMP(3) NOT NULL 52 | ); 53 | 54 | -- CreateIndex 55 | CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); 56 | 57 | -- CreateIndex 58 | CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); 59 | 60 | -- CreateIndex 61 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 62 | 63 | -- CreateIndex 64 | CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); 65 | 66 | -- CreateIndex 67 | CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); 68 | 69 | -- AddForeignKey 70 | ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 71 | 72 | -- AddForeignKey 73 | ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 74 | -------------------------------------------------------------------------------- /prisma/migrations/20240704105613_add_org_and_pr_schema/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Organisations" ( 3 | "id" INTEGER NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "avatar_url" TEXT DEFAULT 'https://www.campusfrance.org/sites/default/files/styles/mobile_visuel_principal_page/public/organisation%20organigramme_3.jpg?itok=qD2R_LHp', 6 | "github_url" TEXT, 7 | 8 | CONSTRAINT "Organisations_pkey" PRIMARY KEY ("id") 9 | ); 10 | 11 | -- CreateTable 12 | CREATE TABLE "PullRequest" ( 13 | "id" TEXT NOT NULL, 14 | "prURL" TEXT NOT NULL, 15 | "prTitle" TEXT NOT NULL, 16 | "prNumber" INTEGER NOT NULL, 17 | "repoURL" TEXT NOT NULL, 18 | "userName" TEXT NOT NULL, 19 | "avatar" TEXT NOT NULL, 20 | "commentURL" TEXT NOT NULL, 21 | "isVerified" BOOLEAN NOT NULL, 22 | "mergedAt" TIMESTAMP(3) NOT NULL, 23 | "body" TEXT, 24 | "prPoint" INTEGER NOT NULL, 25 | "draft" BOOLEAN NOT NULL, 26 | "bounty" TEXT, 27 | "userId" TEXT NOT NULL, 28 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 29 | "updatedAt" TIMESTAMP(3) NOT NULL, 30 | 31 | CONSTRAINT "PullRequest_pkey" PRIMARY KEY ("id") 32 | ); 33 | 34 | -- AddForeignKey 35 | ALTER TABLE "PullRequest" ADD CONSTRAINT "PullRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 36 | -------------------------------------------------------------------------------- /prisma/migrations/20240706190459_added_pr_org_rel/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `orgId` to the `PullRequest` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "PullRequest" ADD COLUMN "orgId" INTEGER NOT NULL; 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "PullRequest" ADD CONSTRAINT "PullRequest_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Organisations"("id") ON DELETE CASCADE ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /prisma/migrations/20240709132951_username_made_unique/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[username]` on the table `User` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240715160927_bounty_feild_type_updated/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The `bounty` column on the `PullRequest` table would be dropped and recreated. This will lead to data loss if there is data in the column. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "PullRequest" DROP COLUMN "bounty", 9 | ADD COLUMN "bounty" INTEGER; 10 | -------------------------------------------------------------------------------- /prisma/migrations/20240718162529_add_new_user_fields/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "githubProfile" TEXT, 3 | ADD COLUMN "isProfilePublic" BOOLEAN NOT NULL DEFAULT true, 4 | ADD COLUMN "linkedInProfile" TEXT, 5 | ADD COLUMN "xProfile" TEXT; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20240729074221_link_user_and_organisation/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "_UserContributions" ( 3 | "A" INTEGER NOT NULL, 4 | "B" TEXT NOT NULL 5 | ); 6 | 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "_UserContributions_AB_unique" ON "_UserContributions"("A", "B"); 9 | 10 | -- CreateIndex 11 | CREATE INDEX "_UserContributions_B_index" ON "_UserContributions"("B"); 12 | 13 | -- AddForeignKey 14 | ALTER TABLE "_UserContributions" ADD CONSTRAINT "_UserContributions_A_fkey" FOREIGN KEY ("A") REFERENCES "Organisations"("id") ON DELETE CASCADE ON UPDATE CASCADE; 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "_UserContributions" ADD CONSTRAINT "_UserContributions_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /prisma/migrations/20240930123222_saved_by_field_added_bw_user_and_org/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "_UserSavedOrganisations" ( 3 | "A" INTEGER NOT NULL, 4 | "B" TEXT NOT NULL 5 | ); 6 | 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "_UserSavedOrganisations_AB_unique" ON "_UserSavedOrganisations"("A", "B"); 9 | 10 | -- CreateIndex 11 | CREATE INDEX "_UserSavedOrganisations_B_index" ON "_UserSavedOrganisations"("B"); 12 | 13 | -- AddForeignKey 14 | ALTER TABLE "_UserSavedOrganisations" ADD CONSTRAINT "_UserSavedOrganisations_A_fkey" FOREIGN KEY ("A") REFERENCES "Organisations"("id") ON DELETE CASCADE ON UPDATE CASCADE; 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "_UserSavedOrganisations" ADD CONSTRAINT "_UserSavedOrganisations_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /prisma/migrations/20241014104957_added_profile_views_schema/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "ProfileView" ( 3 | "id" TEXT NOT NULL, 4 | "userId" TEXT NOT NULL, 5 | "viewedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | "viewerIp" TEXT NOT NULL, 7 | 8 | CONSTRAINT "ProfileView_pkey" PRIMARY KEY ("id") 9 | ); 10 | 11 | -- AddForeignKey 12 | ALTER TABLE "ProfileView" ADD CONSTRAINT "ProfileView_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 13 | -------------------------------------------------------------------------------- /prisma/migrations/20241014123222_updated_few_feild_in_profile_view/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `viewedAt` on the `ProfileView` table. All the data in the column will be lost. 5 | - A unique constraint covering the columns `[userId,viewerIp]` on the table `ProfileView` will be added. If there are existing duplicate values, this will fail. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "ProfileView" DROP COLUMN "viewedAt", 10 | ADD COLUMN "lastViewedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 11 | ADD COLUMN "viewCount" INTEGER NOT NULL DEFAULT 1; 12 | 13 | -- CreateIndex 14 | CREATE UNIQUE INDEX "ProfileView_userId_viewerIp_key" ON "ProfileView"("userId", "viewerIp"); 15 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | model Account { 17 | id String @id @default(cuid()) 18 | userId String 19 | type String 20 | provider String 21 | providerAccountId String 22 | refresh_token String? @db.Text 23 | refresh_token_expires_in Int? 24 | access_token String? @db.Text 25 | expires_at Int? 26 | token_type String? 27 | scope String? 28 | id_token String? @db.Text 29 | session_state String? 30 | 31 | createdAt DateTime @default(now()) 32 | updatedAt DateTime @updatedAt 33 | 34 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 35 | 36 | @@unique([provider, providerAccountId]) 37 | } 38 | 39 | model Session { 40 | id String @id @default(cuid()) 41 | sessionToken String @unique 42 | userId String 43 | expires DateTime 44 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 45 | } 46 | 47 | model User { 48 | id String @id @default(cuid()) 49 | name String? 50 | username String? @unique 51 | email String? @unique 52 | emailVerified DateTime? 53 | image String? 54 | isProfilePublic Boolean @default(true) 55 | githubProfile String? 56 | xProfile String? 57 | linkedInProfile String? 58 | admin Boolean @default(false) 59 | accounts Account[] 60 | sessions Session[] 61 | pullRequests PullRequest[] 62 | savedOrgs Organisations[] @relation("UserSavedOrganisations") 63 | contributedOrgs Organisations[] @relation("UserContributions") 64 | createdAt DateTime @default(now()) 65 | updatedAt DateTime @updatedAt 66 | ProfileView ProfileView[] 67 | } 68 | 69 | model ProfileView { 70 | id String @id @default(cuid()) 71 | userId String 72 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 73 | viewerIp String 74 | viewCount Int @default(1) 75 | lastViewedAt DateTime @default(now()) 76 | 77 | @@unique([userId, viewerIp]) 78 | } 79 | 80 | model Organisations { 81 | id Int @id 82 | name String 83 | avatar_url String? @default("https://www.campusfrance.org/sites/default/files/styles/mobile_visuel_principal_page/public/organisation%20organigramme_3.jpg?itok=qD2R_LHp") 84 | github_url String? 85 | pullRequests PullRequest[] 86 | contributors User[] @relation("UserContributions") 87 | savedBy User[] @relation("UserSavedOrganisations") 88 | } 89 | 90 | model PullRequest { 91 | id String @id @default(cuid()) 92 | prURL String 93 | prTitle String 94 | prNumber Int 95 | repoURL String 96 | userName String 97 | avatar String 98 | commentURL String 99 | isVerified Boolean 100 | mergedAt DateTime 101 | body String? 102 | prPoint Int 103 | draft Boolean 104 | bounty Int? // Nullable field for bounty information 105 | 106 | orgId Int 107 | org Organisations @relation(fields: [orgId], references: [id], onDelete: Cascade) 108 | 109 | userId String // Foreign key relation to User model 110 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 111 | 112 | createdAt DateTime @default(now()) 113 | updatedAt DateTime @updatedAt 114 | } 115 | 116 | model VerificationToken { 117 | identifier String 118 | token String @unique 119 | expires DateTime 120 | 121 | @@unique([identifier, token]) 122 | } 123 | -------------------------------------------------------------------------------- /public/custom-cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanbairagi30/merged-n-share/96b785f6b23f810a706373349c75d34257d0537c/public/custom-cursor.png -------------------------------------------------------------------------------- /public/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanbairagi30/merged-n-share/96b785f6b23f810a706373349c75d34257d0537c/public/dashboard.png -------------------------------------------------------------------------------- /public/desktop-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanbairagi30/merged-n-share/96b785f6b23f810a706373349c75d34257d0537c/public/desktop-image.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/actions/fetchPRs.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { revalidatePath } from 'next/cache'; 4 | 5 | export async function fetchPRs(username: string | undefined, orgName: string) { 6 | if (!username || !orgName) { 7 | throw new Error('Missing username or organization name'); 8 | } 9 | 10 | const token = process.env.NEXT_PUBLIC_GITHUB_AUTH_TOKEN; 11 | if (!token) { 12 | throw new Error('GitHub token is not set'); 13 | } 14 | 15 | try { 16 | const response = await fetch( 17 | `https://api.github.com/search/issues?q=type:pr+author:${username}+org:${orgName}+is:merged`, 18 | { 19 | method: 'GET', 20 | headers: { 21 | Authorization: `Bearer ${token}`, 22 | Accept: 'application/vnd.github.v3+json', 23 | }, 24 | next: { revalidate: 60 }, 25 | }, 26 | ); 27 | 28 | if (!response.ok) { 29 | const errorBody = await response.text(); 30 | console.error('GitHub API Error:', response.status, errorBody); 31 | throw new Error( 32 | `Failed to fetch PR details: ${response.status} ${response.statusText}`, 33 | ); 34 | } 35 | 36 | const data = await response.json(); 37 | revalidatePath('/'); 38 | return data; 39 | } catch (error) { 40 | console.error('Error fetching PR details:', error); 41 | throw new Error('An error occurred while fetching PR details'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/actions/userAction.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import prisma from '@/lib/db'; 4 | 5 | export async function updateUserContributedOrgs(userId: any, orgIds: any[]) { 6 | try { 7 | const connectData = orgIds.map((orgId: any) => ({ id: orgId })); 8 | 9 | await prisma.user.update({ 10 | where: { id: userId }, 11 | data: { 12 | contributedOrgs: { 13 | connect: connectData, 14 | }, 15 | }, 16 | }); 17 | } catch (error) { 18 | console.error('Error updating user contributed orgs:', error); 19 | throw new Error('Failed to update user contributed organizations'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/api/analytics/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { headers } from 'next/headers'; 3 | import prisma from '@/lib/db'; 4 | import { getServerSession } from 'next-auth'; 5 | import { authOptions } from '@/lib/auth'; 6 | 7 | const COOLDOWN_PERIOD = 24 * 60 * 60 * 1000; // 24 hours in milliseconds 8 | 9 | export async function POST(req: NextRequest) { 10 | try { 11 | const { userId } = await req.json(); 12 | const headersList = headers(); 13 | const forwardedFor = headersList.get('x-forwarded-for'); 14 | const viewerIp = forwardedFor ? forwardedFor.split(',')[0] : 'Unknown'; 15 | 16 | const now = new Date(); 17 | 18 | // Check if a view from this IP already exists for this user 19 | const existingView = await prisma.profileView.findUnique({ 20 | where: { 21 | userId_viewerIp: { 22 | userId: userId, 23 | viewerIp: viewerIp, 24 | }, 25 | }, 26 | }); 27 | 28 | if (existingView) { 29 | const timeSinceLastView = 30 | now.getTime() - existingView.lastViewedAt.getTime(); 31 | 32 | if (timeSinceLastView < COOLDOWN_PERIOD) { 33 | // If the cooldown period hasn't passed, don't update the view count 34 | return NextResponse.json( 35 | { message: 'View recorded recently', isNewView: false }, 36 | { status: 200 }, 37 | ); 38 | } 39 | 40 | // Update the existing view's timestamp and increment the view count 41 | await prisma.profileView.update({ 42 | where: { id: existingView.id }, 43 | data: { 44 | lastViewedAt: now, 45 | viewCount: { increment: 1 }, 46 | }, 47 | }); 48 | return NextResponse.json( 49 | { message: 'View updated successfully', isNewView: false }, 50 | { status: 200 }, 51 | ); 52 | } else { 53 | // Create a new view 54 | await prisma.profileView.create({ 55 | data: { 56 | userId, 57 | viewerIp, 58 | lastViewedAt: now, 59 | }, 60 | }); 61 | return NextResponse.json( 62 | { message: 'New view recorded successfully', isNewView: true }, 63 | { status: 200 }, 64 | ); 65 | } 66 | } catch (error) { 67 | console.error('Error recording view:', error); 68 | return NextResponse.json( 69 | { message: 'Error recording view' }, 70 | { status: 500 }, 71 | ); 72 | } 73 | } 74 | 75 | export async function GET(req: NextRequest) { 76 | const session = await getServerSession(authOptions); 77 | const { searchParams } = new URL(req.url); 78 | const userId = session?.user?.id; 79 | const range = searchParams.get('range') || '7d'; // Default to 7 days 80 | console.log(range); 81 | 82 | if (!userId) { 83 | return NextResponse.json({ error: 'User ID is required' }, { status: 400 }); 84 | } 85 | 86 | const now = new Date(); 87 | let startDate: Date; 88 | 89 | switch (range) { 90 | case '24h': 91 | startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); 92 | break; 93 | case '7d': 94 | startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); 95 | break; 96 | case '30d': 97 | startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); 98 | break; 99 | default: 100 | return NextResponse.json({ error: 'Invalid range' }, { status: 400 }); 101 | } 102 | 103 | try { 104 | const views = await prisma.profileView.findMany({ 105 | where: { 106 | userId: userId, 107 | lastViewedAt: { 108 | gte: startDate, 109 | }, 110 | }, 111 | orderBy: { 112 | lastViewedAt: 'asc', 113 | }, 114 | }); 115 | 116 | const groupedViews = views.reduce( 117 | (acc, view) => { 118 | const date = view.lastViewedAt.toISOString().split('T')[0]; 119 | acc[date] = (acc[date] || 0) + 1; 120 | return acc; 121 | }, 122 | {} as Record, 123 | ); 124 | 125 | const formattedData = Object.entries(groupedViews).map(([date, count]) => ({ 126 | date, 127 | views: count, 128 | })); 129 | 130 | return NextResponse.json(formattedData); 131 | } catch (error) { 132 | console.error('Error fetching profile views:', error); 133 | return NextResponse.json( 134 | { error: 'Error fetching profile views' }, 135 | { status: 500 }, 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth/next'; 2 | import { authOptions } from '../../../../lib/auth'; 3 | 4 | const handler = NextAuth(authOptions); 5 | export { handler as GET, handler as POST }; 6 | -------------------------------------------------------------------------------- /src/app/api/leaderboard/route.ts: -------------------------------------------------------------------------------- 1 | import prisma from '@/lib/db'; 2 | import { NextResponse } from 'next/server'; 3 | 4 | export async function GET(req: Request) { 5 | try { 6 | const leaderboard = await prisma.user.findMany({ 7 | select: { 8 | id: true, 9 | name: true, 10 | username: true, 11 | image: true, 12 | pullRequests: { 13 | where: { 14 | isVerified: true, 15 | }, 16 | select: { 17 | prPoint: true, 18 | bounty: true, 19 | }, 20 | }, 21 | }, 22 | }); 23 | 24 | const leaderBoardWithTotalPoints = leaderboard.map((user) => ({ 25 | ...user, 26 | totalPoints: user.pullRequests.reduce((acc, pr) => acc + pr.prPoint, 0), 27 | bounties: user.pullRequests.reduce( 28 | (acc, pr) => acc + (pr?.bounty ? pr?.bounty : 0), 29 | 0, 30 | ), 31 | })); 32 | 33 | leaderBoardWithTotalPoints.sort((a, b) => b.totalPoints - a.totalPoints); 34 | return NextResponse.json({ 35 | success: true, 36 | data: leaderBoardWithTotalPoints, 37 | message: 'Leaderboard sorted and fetched successfully', 38 | }); 39 | } catch (error) { 40 | console.error('Error fetch leaderboard:', error); 41 | return NextResponse.json({ 42 | success: false, 43 | message: 'Failed to create pull requests', 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/api/og-images/public-profile/route.tsx: -------------------------------------------------------------------------------- 1 | import { ImageResponse } from 'next/og'; 2 | 3 | export const runtime = 'edge'; 4 | 5 | export async function GET(req: Request) { 6 | // const usdcBuffer = await fetch(new URL('../../../../public/usdc.svg', import.meta.url)).then( 7 | // (res) => res.arrayBuffer() 8 | // ); 9 | // const usdcBase64 = Buffer.from(usdcBuffer).toString('base64'); 10 | // // ------------ 11 | // const usdtBuffer = await fetch(new URL('../../../../public/usdt.svg', import.meta.url)).then( 12 | // (res) => res.arrayBuffer() 13 | // ); 14 | // const usdtBase64 = Buffer.from(usdtBuffer).toString('base64'); 15 | // // ------------ 16 | 17 | // const solBuffer = await fetch(new URL('../../../../public/sol.svg', import.meta.url)).then( 18 | // (res) => res.arrayBuffer() 19 | // ); 20 | // const solBase64 = Buffer.from(solBuffer).toString('base64'); 21 | // // ------------ 22 | 23 | // const usdc = `data:image/svg+xml;base64,${usdcBase64}`; 24 | // const usdt = `data:image/svg+xml;base64,${usdtBase64}`; 25 | // const sol = `data:image/svg+xml;base64,${solBase64}`; 26 | 27 | const { searchParams } = new URL(req.url); 28 | 29 | const hasUserName = searchParams.has('username'); 30 | const hasName = searchParams.has('name'); 31 | const hasOrg = searchParams.has('org'); 32 | const hasPR = searchParams.has('pr'); 33 | 34 | const userName = hasUserName 35 | ? searchParams.get('username') 36 | : 'merged-n-share'; 37 | const name = hasName ? searchParams.get('name') : 'Test'; 38 | const org = hasOrg ? searchParams.get('org') : '0'; 39 | const pr = hasPR ? searchParams.get('pr') : '0'; 40 | 41 | console.log(userName); 42 | console.log(name); 43 | 44 | const fontData = await fetch( 45 | new URL('../../../fonts/main-font.ttf', import.meta.url), 46 | ).then((res) => res.arrayBuffer()); 47 | 48 | const fontDataSecondary = await fetch( 49 | new URL('../../../fonts/CircularStd-Bold.ttf', import.meta.url), 50 | ).then((res) => res.arrayBuffer()); 51 | 52 | return new ImageResponse( 53 | ( 54 |
55 |
56 | 60 | 64 |
68 | Merged&Share{' '} 69 |
70 |
71 |
72 |

{name}

73 |

github.com/{userName}

74 |
75 | 76 |
77 |
78 | 79 | Merged PRs 80 | 81 | 82 | {pr} 83 | 84 |
85 |
86 | 87 | Organisation Contributed 88 | 89 | 90 | {org} 91 | 92 |
93 |
94 | 95 |
113 |
125 | M &S. 126 |
127 |
128 | ), 129 | 130 | { 131 | fonts: [ 132 | { 133 | name: 'main', 134 | data: fontData, 135 | style: 'normal', 136 | }, 137 | { 138 | name: 'secondary', 139 | data: fontDataSecondary, 140 | style: 'normal', 141 | }, 142 | ], 143 | }, 144 | ); 145 | } 146 | -------------------------------------------------------------------------------- /src/app/api/organisation/route.ts: -------------------------------------------------------------------------------- 1 | import { authOptions } from '@/lib/auth'; 2 | import prisma from '@/lib/db'; 3 | import { Organisations } from '@prisma/client'; 4 | import { getServerSession } from 'next-auth'; 5 | import { NextResponse } from 'next/server'; 6 | 7 | // to approve or disapprove an organisation 8 | export async function POST(req: Request) { 9 | const session = await getServerSession(authOptions); 10 | 11 | try { 12 | const { id, name, github_url, avatar_url } = await req.json(); 13 | 14 | console.log(session?.user); 15 | 16 | // @ts-ignore 17 | const userId = session?.user?.id; 18 | 19 | const existingSave = await prisma.user.findFirst({ 20 | where: { 21 | id: userId, 22 | savedOrgs: { some: { id: parseInt(id) } }, 23 | }, 24 | }); 25 | 26 | if (existingSave) { 27 | // User has already saved this org, so unsave it 28 | await prisma.user.update({ 29 | where: { id: userId }, 30 | data: { 31 | savedOrgs: { disconnect: { id: parseInt(id) } }, 32 | }, 33 | }); 34 | return NextResponse.json({ 35 | success: true, 36 | msg: 'Organisation unsaved successfully', 37 | action: 'unsaved', 38 | }); 39 | } else { 40 | // User hasn't saved this org, so save it 41 | // First, ensure the organisation exists 42 | const org = await prisma.organisations.upsert({ 43 | where: { id: parseInt(id) }, 44 | update: {}, // If it exists, don't update anything 45 | create: { 46 | id: parseInt(id), 47 | name, 48 | github_url, 49 | avatar_url, 50 | }, 51 | }); 52 | 53 | // Then, connect the user to the organisation 54 | await prisma.user.update({ 55 | where: { id: userId }, 56 | data: { 57 | savedOrgs: { connect: { id: org.id } }, 58 | }, 59 | }); 60 | return NextResponse.json({ 61 | success: true, 62 | msg: 'Organisation saved successfully', 63 | action: 'saved', 64 | }); 65 | } 66 | } catch (error) { 67 | console.error('Error creating pull requests:', error); 68 | return NextResponse.json({ success: false, message: 'Failed' }); 69 | } 70 | } 71 | 72 | // to get all the orgnisations 73 | export async function GET(req: Request) { 74 | const session = await getServerSession(authOptions); 75 | 76 | try { 77 | //@ts-ignore 78 | const userId = session?.user?.id; 79 | 80 | if (!userId) { 81 | return NextResponse.json({ 82 | success: false, 83 | message: 'User ID not found', 84 | }); 85 | } 86 | //@ts-ignore 87 | // if (session?.user?.admin == false) { 88 | // return NextResponse.json({ success: false, message: "UnAuthorized User" }, { status: 403 }); 89 | // } 90 | 91 | const user = await prisma.user.findUnique({ 92 | where: { id: userId }, 93 | include: { savedOrgs: true }, 94 | }); 95 | 96 | if (user) { 97 | return NextResponse.json( 98 | { success: true, organisations: user.savedOrgs }, 99 | { status: 200 }, 100 | ); 101 | } else { 102 | return NextResponse.json( 103 | { success: false, message: 'User not found' }, 104 | { status: 404 }, 105 | ); 106 | } 107 | } catch (error) { 108 | console.error('Error creating pull requests:', error); 109 | return NextResponse.json({ success: false, message: 'Failed' }); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/app/api/pr/[username]/route.ts: -------------------------------------------------------------------------------- 1 | import prisma from '@/lib/db'; 2 | import { NextRequest, NextResponse } from 'next/server'; 3 | 4 | export async function GET( 5 | req: NextRequest, 6 | { params: { username } }: { params: { username: string } }, 7 | ) { 8 | try { 9 | if (!username) { 10 | return NextResponse.json({ 11 | success: false, 12 | message: 'Please correctly type the username and retry again!', 13 | }); 14 | } 15 | 16 | const userData = await prisma.user.findUnique({ 17 | where: { 18 | username: username, 19 | }, 20 | include: { 21 | contributedOrgs: { 22 | select: { 23 | id: true, 24 | name: true, 25 | avatar_url: true, 26 | }, 27 | }, 28 | pullRequests: { 29 | select: { 30 | prURL: true, 31 | avatar: true, 32 | mergedAt: true, 33 | id: true, 34 | prNumber: true, 35 | repoURL: true, 36 | body: true, 37 | prTitle: true, 38 | userName: true, 39 | org: { 40 | select: { 41 | id: true, 42 | name: true, 43 | avatar_url: true, 44 | github_url: true, 45 | }, 46 | }, 47 | }, 48 | }, 49 | }, 50 | }); 51 | if (!userData) { 52 | return NextResponse.json({ 53 | success: true, 54 | message: "We can't find any user with this username in the database", 55 | }); 56 | } 57 | 58 | if (userData?.pullRequests.length === 0) { 59 | return NextResponse.json({ 60 | success: true, 61 | message: 62 | 'No Pull requests found for this username , try saving the new prs from my-pr section in Merged&Share', 63 | }); 64 | } 65 | 66 | return NextResponse.json({ 67 | success: true, 68 | userData: userData, 69 | message: 'Merged PRs with this username has been found successfully ', 70 | }); 71 | } catch (error) { 72 | console.error('Error creating pull requests:', error); 73 | return NextResponse.json({ 74 | success: false, 75 | message: 'Failed to create pull requests', 76 | }); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/api/pr/route.ts: -------------------------------------------------------------------------------- 1 | import { authOptions } from '@/lib/auth'; 2 | import prisma from '@/lib/db'; 3 | import { PullRequest } from '@prisma/client'; 4 | import { getServerSession } from 'next-auth'; 5 | import { NextResponse } from 'next/server'; 6 | 7 | function getPRPointsByBounty(bountyAmount: number) { 8 | // const bountyAmount = Number(typeof bt === 'string' && bt.replace("$", "")); 9 | if (bountyAmount === 0 || !bountyAmount) { 10 | return 1; 11 | } else if (bountyAmount > 0 && bountyAmount <= 30) { 12 | return 10; 13 | } else if (bountyAmount > 30 && bountyAmount <= 60) { 14 | return 20; 15 | } else if (bountyAmount > 60 && bountyAmount <= 80) { 16 | return 30; 17 | } else if (bountyAmount > 80 && bountyAmount <= 150) { 18 | return 40; 19 | } else if (bountyAmount > 150 && bountyAmount <= 250) { 20 | return 50; 21 | } else if (bountyAmount > 250 && bountyAmount <= 500) { 22 | return 60; 23 | } else if (bountyAmount > 500) { 24 | return 70; 25 | } 26 | } 27 | 28 | export async function GET(req: Request) { 29 | const session = await getServerSession(authOptions); 30 | //@ts-ignore 31 | const userId = session?.user?.id; 32 | try { 33 | if (!userId) { 34 | return NextResponse.json({ 35 | success: false, 36 | message: 'User ID not found', 37 | }); 38 | } 39 | 40 | const savedPullRequests = await prisma.pullRequest.findMany({ 41 | where: { 42 | userId: userId, 43 | }, 44 | include: { 45 | org: { 46 | select: { 47 | name: true, 48 | avatar_url: true, 49 | github_url: true, 50 | }, 51 | }, 52 | }, 53 | }); 54 | 55 | console.log('Saved pull requests:', savedPullRequests); 56 | return NextResponse.json({ 57 | success: true, 58 | pullRequests: savedPullRequests, 59 | }); 60 | } catch (error) { 61 | console.error('Error creating pull requests:', error); 62 | return NextResponse.json({ 63 | success: false, 64 | message: 'Failed to create pull requests', 65 | }); 66 | } 67 | return NextResponse.json({ message: 'Working' }); 68 | } 69 | export async function POST(req: Request) { 70 | const body = await req.json(); 71 | console.log(body); 72 | const session = await getServerSession(authOptions); 73 | 74 | const pullRequestsData = body.prs.map((prData: any) => { 75 | const pointPerPRWrtBounty = getPRPointsByBounty(prData.bounty[0]); 76 | return { 77 | prURL: prData.prURL, 78 | prTitle: prData.prTitle, 79 | prNumber: prData.prNumber, 80 | repoURL: prData.repoURL, 81 | userName: prData.userName, 82 | avatar: prData.avatar, 83 | commentURL: prData.commentURL, 84 | isVerified: prData.isVerified, 85 | mergedAt: prData.mergedAt, 86 | body: prData.body, 87 | draft: prData.draft, 88 | bounty: prData.bounty[0], 89 | prPoint: pointPerPRWrtBounty, 90 | orgId: prData.organisationId, 91 | //@ts-ignore 92 | userId: session?.user?.id, 93 | }; 94 | }); 95 | try { 96 | const existingPRs = await prisma.pullRequest.findMany({ 97 | where: { 98 | prNumber: { 99 | in: pullRequestsData.map((pr: PullRequest) => pr.prNumber), 100 | }, 101 | //@ts-ignore 102 | userId: session?.user?.id, 103 | }, 104 | }); 105 | 106 | // TO FIX : Edge case missing : when a user have a PR ex : #120 merged in repo 1 and #120 merged in repo 2 then below logic can't handle it . 107 | const newPullRequestsData = pullRequestsData.filter( 108 | (prData: PullRequest) => { 109 | return !existingPRs.some( 110 | (existingPR) => 111 | existingPR.prNumber === prData.prNumber && 112 | existingPR.repoURL === prData.repoURL, 113 | ); 114 | }, 115 | ); 116 | 117 | if (newPullRequestsData.length > 0) { 118 | const createdPullRequests = await prisma.pullRequest.createMany({ 119 | data: newPullRequestsData, 120 | }); 121 | 122 | console.log('Pull requests created:', createdPullRequests); 123 | return NextResponse.json({ 124 | success: true, 125 | message: 'Pull requests created successfully', 126 | }); 127 | } else { 128 | console.log('No new pull requests to create'); 129 | return NextResponse.json({ 130 | success: false, 131 | message: 'No new pull requests to create', 132 | }); 133 | } 134 | } catch (error) { 135 | console.error('Error creating pull requests:', error); 136 | return NextResponse.json({ 137 | success: false, 138 | message: 'Failed to create pull requests', 139 | }); 140 | } 141 | } 142 | 143 | export async function DELETE(req: Request) { 144 | const { prId } = await req.json(); 145 | const session = await getServerSession(authOptions); 146 | const userId = session?.user.id; 147 | 148 | if (!userId) { 149 | return NextResponse.json( 150 | { success: false, message: 'Unauthorised' }, 151 | { status: 400 }, 152 | ); 153 | } 154 | 155 | try { 156 | const pullRequest = await prisma.pullRequest.findUnique({ 157 | where: { 158 | id: prId, 159 | userId: userId, 160 | }, 161 | }); 162 | 163 | if (!pullRequest) { 164 | return NextResponse.json( 165 | { 166 | success: false, 167 | message: 168 | "Pull Request not found or you don't have permission to delete it", 169 | }, 170 | { status: 404 }, 171 | ); 172 | } 173 | 174 | await prisma.pullRequest.delete({ 175 | where: { 176 | id: prId, 177 | }, 178 | }); 179 | 180 | return NextResponse.json({ 181 | success: true, 182 | message: 'Pull Request has been deleted', 183 | }); 184 | } catch (error) { 185 | console.error('Error creating pull requests:', error); 186 | return NextResponse.json({ 187 | success: false, 188 | message: 'Failed to delete pull requests', 189 | }); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/app/api/user/route.ts: -------------------------------------------------------------------------------- 1 | import { authOptions } from '@/lib/auth'; 2 | import prisma from '@/lib/db'; 3 | import { getServerSession } from 'next-auth'; 4 | import { NextResponse } from 'next/server'; 5 | 6 | export async function PUT(req: Request) { 7 | const session = await getServerSession(authOptions); 8 | const body = await req.json(); 9 | // @ts-ignore 10 | const userId = session?.user?.id; 11 | try { 12 | if (!userId) { 13 | return NextResponse.json({ 14 | success: false, 15 | message: 'User ID not found', 16 | }); 17 | } 18 | 19 | console.log(body); 20 | 21 | if (body.name !== undefined) { 22 | await prisma.user.updateMany({ 23 | where: { 24 | id: userId, 25 | }, 26 | data: { 27 | name: body.name, 28 | }, 29 | }); 30 | } else if (body.isChecked !== undefined) { 31 | await prisma.user.updateMany({ 32 | where: { 33 | id: userId, 34 | }, 35 | data: { 36 | isProfilePublic: body.isChecked, 37 | }, 38 | }); 39 | } else if (body.socialLinks !== undefined) { 40 | const { x, linkedIn } = body.socialLinks; 41 | await prisma.user.updateMany({ 42 | where: { 43 | id: userId, 44 | }, 45 | data: { 46 | xProfile: x, 47 | linkedInProfile: linkedIn, 48 | }, 49 | }); 50 | } 51 | 52 | return NextResponse.json( 53 | { success: 'true', message: 'Your name has been updated' }, 54 | { status: 200 }, 55 | ); 56 | } catch (error) { 57 | console.log(error); 58 | return NextResponse.json( 59 | { success: 'false', message: 'Something went wrong' }, 60 | { status: 500 }, 61 | ); 62 | } 63 | } 64 | 65 | export async function GET(req: Request) { 66 | const session = await getServerSession(authOptions); 67 | // @ts-ignore 68 | const userId = session?.user?.id; 69 | try { 70 | if (!userId) { 71 | return NextResponse.json({ 72 | success: false, 73 | message: 'User ID not found', 74 | }); 75 | } 76 | 77 | const userData = await prisma.user.findMany({ 78 | where: { 79 | id: userId, 80 | }, 81 | select: { 82 | id: true, 83 | name: true, 84 | email: true, 85 | githubProfile: true, 86 | linkedInProfile: true, 87 | isProfilePublic: true, 88 | updatedAt: true, 89 | xProfile: true, 90 | username: true, 91 | admin: true, 92 | createdAt: true, 93 | pullRequests: true, 94 | contributedOrgs: { 95 | select: { 96 | id: true, 97 | name: true, 98 | avatar_url: true, 99 | }, 100 | }, 101 | }, 102 | }); 103 | 104 | return NextResponse.json( 105 | { 106 | success: 'true', 107 | user: userData, 108 | message: 'Your Details have been fetched successfully', 109 | }, 110 | { status: 200 }, 111 | ); 112 | } catch (error) { 113 | console.log(error); 114 | return NextResponse.json( 115 | { success: 'false', message: 'Something went wrong' }, 116 | { status: 500 }, 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/app/assets/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/assets/google.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/assets/x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/auth/page.tsx: -------------------------------------------------------------------------------- 1 | import { getServerSession } from 'next-auth'; 2 | import { authOptions } from '@/lib/auth'; 3 | import { redirect } from 'next/navigation'; 4 | import SignIn from '@/components/SignIn'; 5 | import { Metadata } from 'next'; 6 | 7 | export const metadata: Metadata = { 8 | title: 'M&S | Authentication', 9 | description: '', 10 | openGraph: { 11 | type: 'website', 12 | title: 'M&S | Authentication', 13 | description: 14 | 'Showcase your open source contributions as Proof of Work by sharing your merged pull requests to anyone around the world with help of Merged&Share', 15 | images: [ 16 | { 17 | url: `${process.env.NEXT_PUBLIC_URL}/api/og-images/root`, 18 | alt: 'og-image-for-home-page', 19 | }, 20 | ], 21 | }, 22 | }; 23 | 24 | const SigninPage = async () => { 25 | const session = await getServerSession(authOptions); 26 | if (session) { 27 | redirect('/work/dashboard'); 28 | } 29 | return ; 30 | }; 31 | 32 | export default SigninPage; 33 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanbairagi30/merged-n-share/96b785f6b23f810a706373349c75d34257d0537c/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/fonts/CircularStd-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanbairagi30/merged-n-share/96b785f6b23f810a706373349c75d34257d0537c/src/app/fonts/CircularStd-Bold.ttf -------------------------------------------------------------------------------- /src/app/fonts/CircularStd-Book.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanbairagi30/merged-n-share/96b785f6b23f810a706373349c75d34257d0537c/src/app/fonts/CircularStd-Book.ttf -------------------------------------------------------------------------------- /src/app/fonts/CircularStd-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanbairagi30/merged-n-share/96b785f6b23f810a706373349c75d34257d0537c/src/app/fonts/CircularStd-Medium.ttf -------------------------------------------------------------------------------- /src/app/fonts/main-font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanbairagi30/merged-n-share/96b785f6b23f810a706373349c75d34257d0537c/src/app/fonts/main-font.ttf -------------------------------------------------------------------------------- /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: 47.9 95.8% 53.1%; 14 | --primary-foreground: 26 83.3% 14.1%; 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: 20 14.3% 4.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: 47.9 95.8% 53.1%; 42 | --primary-foreground: 26 83.3% 14.1%; 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 62.8% 30.6%; 50 | --destructive-foreground: 60 9.1% 97.8%; 51 | --border: 12 6.5% 15.1%; 52 | --input: 12 6.5% 15.1%; 53 | --ring: 35.5 91.7% 32.9%; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | --chart-5: 47.9 95.8% 53.1%; 60 | } 61 | } 62 | 63 | @layer base { 64 | * { 65 | @apply border-border; 66 | } 67 | 68 | p { 69 | @apply font-paragraph; 70 | } 71 | 72 | body { 73 | @apply bg-background text-foreground; 74 | scroll-behavior: smooth; 75 | } 76 | } 77 | 78 | .clip-text { 79 | -webkit-background-clip: text; 80 | background-clip: text; 81 | } 82 | -------------------------------------------------------------------------------- /src/app/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | export function useDebounce(value: string, delay: number) { 5 | const [debouncedValue, setDebouncedValue] = useState(value); 6 | 7 | useEffect(() => { 8 | const handler = setTimeout(() => { 9 | setDebouncedValue(value); 10 | }, delay); 11 | 12 | return () => { 13 | clearTimeout(handler); 14 | }; 15 | }, [value, delay]); 16 | 17 | return debouncedValue; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/hooks/useUserData.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | export function useUserData() { 4 | const [userData, setUserData] = useState(null); 5 | 6 | useEffect(() => { 7 | const fetchData = async () => { 8 | const response = await fetch(`${process.env.NEXT_PUBLIC_URL}/api/user`); 9 | const data = await response.json(); 10 | setUserData(data.user[0]); 11 | }; 12 | 13 | fetchData(); 14 | }, []); 15 | 16 | return { userData }; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/hooks/useWebsocket.ts: -------------------------------------------------------------------------------- 1 | import { LeaderboardEntry, WebSocketMessage } from '@/util/types'; 2 | import { useState, useEffect, useCallback } from 'react'; 3 | 4 | const useWebSocket = (url: string) => { 5 | const [ws, setWs] = useState(null); 6 | const [isConnected, setIsConnected] = useState(false); 7 | const [leaderboard, setLeaderboard] = useState([]); 8 | const [message, setMessage] = useState(''); 9 | const [isFetching, setIsFetching] = useState(false); 10 | 11 | const connect = useCallback(() => { 12 | const websocket = new WebSocket(url); 13 | 14 | websocket.onopen = () => { 15 | console.log('WebSocket connected'); 16 | setIsConnected(true); 17 | }; 18 | 19 | websocket.onmessage = (event: MessageEvent) => { 20 | console.log(event); 21 | const data: WebSocketMessage = JSON.parse(event.data); 22 | switch (data.type) { 23 | case 'leaderboard': 24 | if (data.data) { 25 | setLeaderboard(data.data); 26 | setIsFetching(false); 27 | } 28 | break; 29 | case 'welcome': 30 | if (data.message) setMessage(data.message); 31 | break; 32 | default: 33 | console.log('Received message:', data); 34 | } 35 | }; 36 | 37 | websocket.onclose = () => { 38 | console.log('WebSocket disconnected'); 39 | setIsConnected(false); 40 | // Attempt to reconnect after a delay 41 | setTimeout(connect, 3000); 42 | }; 43 | 44 | setWs(websocket); 45 | }, [url]); 46 | 47 | useEffect(() => { 48 | connect(); 49 | return () => { 50 | if (ws) { 51 | ws.close(); 52 | } 53 | }; 54 | }, [connect]); 55 | 56 | const requestUpdate = useCallback(() => { 57 | if (ws && isConnected) { 58 | setIsFetching(true); 59 | ws.send(JSON.stringify({ type: 'requestUpdate' })); 60 | } 61 | }, [ws, isConnected]); 62 | 63 | return { isConnected, leaderboard, message, isFetching, requestUpdate }; 64 | }; 65 | 66 | export default useWebSocket; 67 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Bricolage_Grotesque, Epilogue, Work_Sans } from 'next/font/google'; 3 | import './globals.css'; 4 | import Providers from '@/components/Providers'; 5 | import NextTopLoader from 'nextjs-toploader'; 6 | import { Toaster } from '@/components/ui/sonner'; 7 | import { Analytics } from '@vercel/analytics/react'; 8 | import localFont from 'next/font/local'; 9 | import { ThemeProvider } from '@/components/theme-provider'; 10 | import { cn } from '@/lib/utils'; 11 | import Script from 'next/script'; 12 | import GoogleAnalytics from '@/components/google-analytics'; 13 | 14 | const circular = localFont({ 15 | variable: '--font-paragraph', 16 | display: 'swap', 17 | src: [ 18 | { 19 | path: './fonts/CircularStd-Bold.ttf', 20 | weight: '700', 21 | style: 'normal', 22 | }, 23 | { 24 | path: './fonts/CircularStd-Medium.ttf', 25 | weight: '500', 26 | style: 'normal', 27 | }, 28 | { 29 | path: './fonts/CircularStd-Book.ttf', 30 | weight: '400', 31 | style: 'normal', 32 | }, 33 | ], 34 | }); 35 | 36 | // primary font 37 | const work_sans = Work_Sans({ 38 | subsets: ['latin'], 39 | weight: ['200', '300', '500', '600', '700', '800'], 40 | variable: '--font-primary', 41 | display: 'swap', 42 | }); 43 | 44 | // secondary font 45 | const bricolage = Bricolage_Grotesque({ 46 | subsets: ['latin'], 47 | weight: ['200', '300', '500', '600', '700', '800'], 48 | variable: '--font-secondary', 49 | display: 'swap', 50 | }); 51 | 52 | // // paragraph/text font 53 | // const epilogue = Epilogue({ 54 | // subsets: ["latin"], 55 | // weight: ["200", "300", "500", "600", "700", "800"], 56 | // variable: '--font-paragraph', 57 | // display: "swap", 58 | // }); 59 | 60 | export const metadata: Metadata = { 61 | title: 'M&S', 62 | description: '', 63 | openGraph: { 64 | type: 'website', 65 | title: 'M&S', 66 | description: 67 | 'Showcase your open source contributions as Proof of Work by sharing your merged pull requests to anyone around the world with help of Merged&Share', 68 | images: [ 69 | { 70 | url: `${process.env.NEXT_PUBLIC_URL}/api/og-images/root`, 71 | alt: 'og-image-for-home-page', 72 | }, 73 | ], 74 | }, 75 | }; 76 | 77 | export default function RootLayout({ 78 | children, 79 | }: Readonly<{ 80 | children: React.ReactNode; 81 | }>) { 82 | return ( 83 | 84 | 92 | 93 | 94 | 95 | 96 |
{children}
97 |
98 |
99 | 100 | 101 | 105 | 106 | 107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Features from '@/components/features'; 2 | import Footer from '@/components/footer'; 3 | import Hero from '@/components/Hero'; 4 | import HowItWorks from '@/components/how-it-works'; 5 | import { Navbar } from '@/components/Navbar'; 6 | import Statistics from '@/components/statistics'; 7 | import Testimonials from '@/components/testimonials'; 8 | import WallOfLove from '@/components/WallOfLove'; 9 | 10 | export default function Home() { 11 | return ( 12 |
13 |
14 |
15 | 16 |
17 | 18 |
19 | 20 |
21 | 22 | 23 | 24 | {/*
*/} 25 | 26 | 27 |
28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/app/privacy-policy/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const PrivacyPolicy = () => { 4 | return ( 5 |
6 |

Privacy Policy

7 | 8 |
9 |

1. Introduction

10 |

11 | Your privacy is important to us. This Privacy Policy explains how we 12 | collect, use, and protect your information when you use Merged&Share. 13 |

14 |
15 | 16 |
17 |

18 | 2. Information We Collect 19 |

20 |
    21 |
  • 22 | Personal Information: Name, email, and payment details for account 23 | setup and billing. 24 |
  • 25 |
  • 26 | Usage Data: Information about how you interact with the platform. 27 |
  • 28 |
  • Cookies: Used for analytics and improving user experience.
  • 29 |
30 |
31 | 32 |
33 |

34 | 3. How We Use Your Information 35 |

36 |
    37 |
  • To provide and maintain our services.
  • 38 |
  • To process payments and manage subscriptions.
  • 39 |
  • To improve the platform and user experience.
  • 40 |
  • To communicate with you regarding updates and offers.
  • 41 |
42 |
43 | 44 |
45 |

4. Third-Party Sharing

46 |

We may share your information with:

47 |
    48 |
  • Payment processors (e.g., Dodo Payments).
  • 49 |
  • Analytics providers (e.g., Google Analytics).
  • 50 |
  • AI tools used for PR summarization.
  • 51 |
52 |
53 | 54 |
55 |

5. Data Security

56 |

57 | We implement measures to protect your data, but no system is 58 | completely secure. By using Merged&Share, you acknowledge and accept 59 | this risk. 60 |

61 |
62 | 63 |
64 |

6. User Rights

65 |
    66 |
  • Access and update your personal information.
  • 67 |
  • Request data deletion.
  • 68 |
  • Opt out of marketing communications.
  • 69 |
70 |
71 | 72 |
73 |

7. Cookies Policy

74 |

75 | Our website uses cookies to track user activity and improve services. 76 | You can disable cookies through your browser settings. 77 |

78 |
79 | 80 |
81 |

8. Contact Information

82 |

83 | If you have privacy concerns, contact us at{' '} 84 | 85 | amanbairagi1089@gmail.com 86 | 87 | . 88 |

89 |
90 |
91 | ); 92 | }; 93 | 94 | export default PrivacyPolicy; 95 | -------------------------------------------------------------------------------- /src/app/refund-policy/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const RefundPolicy = () => { 4 | return ( 5 |
6 |

Refund Policy

7 | 8 |
9 |

1. Refund Eligibility

10 |

Refunds are available for:

11 |
    12 |
  • 13 | Subscription cancellations requested within 14 days of purchase. 14 |
  • 15 |
  • Accidental charges, provided the service was not used.
  • 16 |
17 |
18 | 19 |
20 |

21 | 2. No Refunds for Used Services 22 |

23 |

24 | Refunds are not available for digital services that have been fully 25 | delivered or consumed, such as completed AI PR summaries. 26 |

27 |
28 | 29 |
30 |

31 | 3. How to Request a Refund 32 |

33 |

34 | To request a refund, email{' '} 35 | 36 | refunds@mergednshare.com 37 | {' '} 38 | with the following details: 39 |

40 |
    41 |
  • Account email.
  • 42 |
  • Reason for the refund request.
  • 43 |
  • Proof of payment (e.g., transaction ID).
  • 44 |
45 |
46 | 47 |
48 |

4. Refund Processing

49 |

50 | Refunds will be processed within 7-10 business days. The amount will 51 | be credited back to the original payment method. 52 |

53 |
54 | 55 |
56 |

57 | 5. Subscription Cancellation 58 |

59 |

60 | Canceling a subscription will prevent further billing, but your 61 | account will retain access to paid features until the end of the 62 | current billing cycle. 63 |

64 |
65 | 66 |
67 |

6. Contact Information

68 |

69 | For refund-related inquiries, contact us at{' '} 70 | 71 | amanbairagi1089@gmail.com 72 | 73 | . 74 |

75 |
76 |
77 | ); 78 | }; 79 | 80 | export default RefundPolicy; 81 | -------------------------------------------------------------------------------- /src/app/terms-and-conditions/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TermsAndConditions = () => { 4 | return ( 5 |
6 |

Terms and Conditions

7 | 8 |
9 |

1. Introduction

10 |

11 | Welcome to Merged&Share, a platform designed to help users showcase 12 | their open-source contributions, share embeds, and utilize advanced 13 | tools like AI PR summarization. By accessing or using our service, you 14 | agree to comply with these Terms and Conditions. 15 |

16 |
17 | 18 |
19 |

2. Eligibility

20 |

21 | You must be at least 18 years old or have parental/guardian consent to 22 | use this platform. By using Merged&Share, you represent and warrant 23 | that you meet this eligibility requirement. 24 |

25 |
26 | 27 |
28 |

3. Account Terms

29 |
    30 |
  • 31 | You are responsible for maintaining the confidentiality of your 32 | account credentials. 33 |
  • 34 |
  • 35 | You must provide accurate and complete information during 36 | registration. 37 |
  • 38 |
  • 39 | Any activity conducted through your account is your responsibility. 40 |
  • 41 |
42 |
43 | 44 |
45 |

4. Free vs Paid Plan

46 |

47 | Free Plan: Includes basic profile sharing with Merged&Share branding. 48 |

49 |

Paid Plan: Offers premium features such as:

50 |
    51 |
  • Watermark-free embeds.
  • 52 |
  • AI PR summarization.
  • 53 |
  • Advanced analytics.
  • 54 |
  • Additional customization options.
  • 55 |
56 |
57 | 58 |
59 |

5. Payment Terms

60 |
    61 |
  • 62 | Paid plans are subscription-based and will auto-renew unless 63 | canceled. 64 |
  • 65 |
  • 66 | Payments are processed securely through third-party providers. 67 |
  • 68 |
  • 69 | You agree to provide current, complete, and accurate payment 70 | information. 71 |
  • 72 |
73 |
74 | 75 |
76 |

77 | 6. Prohibited Activities 78 |

79 |

You may not:

80 |
    81 |
  • Use the service for illegal purposes.
  • 82 |
  • Attempt to hack, overload, or disrupt the platform.
  • 83 |
  • Upload malicious content, such as viruses.
  • 84 |
85 |
86 | 87 |
88 |

7. Termination

89 |

90 | We reserve the right to suspend or terminate your account if you 91 | violate these Terms and Conditions or misuse the platform. 92 |

93 |
94 | 95 |
96 |

8. Changes to Terms

97 |

98 | We may update these Terms and Conditions periodically. Continued use 99 | of the platform after updates constitutes acceptance of the revised 100 | terms. 101 |

102 |
103 | 104 |
105 |

9. Contact Information

106 |

107 | For any questions about these Terms and Conditions, please contact us 108 | at{' '} 109 | 110 | amanbairagi1089@gmail.com 111 | 112 | . 113 |

114 |
115 |
116 | ); 117 | }; 118 | 119 | export default TermsAndConditions; 120 | -------------------------------------------------------------------------------- /src/app/types/global.d.ts: -------------------------------------------------------------------------------- 1 | export interface WidgetWebComponentProps 2 | extends React.DetailedHTMLProps< 3 | React.HTMLAttributes, 4 | HTMLElement 5 | > { 6 | theme?: string | undefined; 7 | username?: string | undefined; 8 | 'lg-cols'?: number; 9 | 'card-view'?: 'list' | 'grid'; 10 | 'font-variable'?: string; 11 | 'md-cols'?: number; 12 | 'base-cols'?: number; 13 | 'top-visible'?: 'true' | 'false'; 14 | } 15 | 16 | declare global { 17 | namespace JSX { 18 | interface IntrinsicElements { 19 | 'widget-web-component': WidgetWebComponentProps; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/work/embed/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState } from 'react'; 4 | import { Badge } from '@/components/ui/badge'; 5 | import { frameWorksData } from '@/data/data'; 6 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; 7 | import { Info, LucideExternalLink, ScanEye, TriangleAlert } from 'lucide-react'; 8 | import { useSession } from 'next-auth/react'; 9 | import { 10 | Sheet, 11 | SheetContent, 12 | SheetDescription, 13 | SheetHeader, 14 | SheetTitle, 15 | SheetTrigger, 16 | } from '@/components/ui/sheet'; 17 | import { Button } from '@/components/ui/button'; 18 | import { useTheme } from 'next-themes'; 19 | import { useUserData } from '@/app/hooks/useUserData'; 20 | import { AlertBox } from '@/components/alert-box'; 21 | import { PreviewWidget } from '@/components/preview-widget'; 22 | import { FrameworkList } from '@/components/frameworks-list'; 23 | import { CodeBlock } from '@/components/code-block'; 24 | 25 | export default function Embed() { 26 | const [selectedTab, setSelectedTab] = React.useState< 27 | 'html' | 'reactjs' | 'nextjs' 28 | >('html'); 29 | const { theme } = useTheme(); 30 | const { data: session } = useSession(); 31 | const username = session?.user?.username; 32 | const { userData } = useUserData(); 33 | 34 | const [mounted, setMounted] = useState(false); 35 | 36 | useEffect(() => { 37 | setMounted(true); 38 | }, []); 39 | 40 | const getCodeString = (tab: 'html' | 'reactjs' | 'nextjs'): string => { 41 | const codeStrings = { 42 | html: ` 43 | 44 | 45 | 46 | 47 | `, 48 | reactjs: ` 49 | 50 | 51 | 52 | 53 | `, 54 | nextjs: ` 55 | // STEP-1 : paste this at src/app/layout.tsx 56 | 57 | import Script from "next/script"; 58 | 59 | 60 | 61 | 62 | `, 63 | }; 64 | 65 | return codeStrings[tab]; 66 | }; 67 | 68 | if (!mounted) { 69 | return null; // or a loading placeholder 70 | } 71 | 72 | return ( 73 |
74 | {userData?.pullRequests?.length === 0 && ( 75 | } 77 | title="No Pull Requests Found" 78 | message={ 79 | <> 80 | It seems you don't have any pull requests saved in the 81 | 82 | Merged&Share 83 | 84 | database. To get started, go to the 85 | window.open('/work/my-pr', '_blank')} 87 | className="mx-1 cursor-pointer bg-accent font-secondary text-foreground hover:bg-accent" 88 | > 89 | My PRs 90 | 91 | section, select your organizations, and save your merged PRs. 92 | 93 | } 94 | /> 95 | )} 96 | 97 |

98 | Embed your PRs to your own website 99 |

100 |
101 | Now you can embed your merged PRs saved in Merged&Share in your{' '} 102 | own website (be it a 103 | portfolio website or any other website) 104 |
105 | 106 | 107 | 108 |
109 |

Usage

110 |

111 | Copy and paste the code in your website. 112 |

113 |
114 | 115 | 116 | 117 | 120 | setSelectedTab(value as 'html' | 'reactjs' | 'nextjs') 121 | } 122 | className="w-full" 123 | > 124 | 125 | HTML 126 | ReactJs 127 | NextJS 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | , 144 | HTMLElement 145 | > { 146 | theme?: string | undefined; 147 | username?: string | undefined; 148 | 'lg-cols'?: number; // sets columns on large screens (works with grid layout only) . 149 | 'card-view'?: 'list' | 'grid'; // toggles between list and grid view of the PR cards 150 | 'fontVariable'?: string; // You can add your font variable (e.g., --font-primary or any custom variable name) in this prop. 151 | 'md-cols'?: number; // sets columns on medium screens (works with grid layout only) . 152 | 'base-cols'?: number; // sets columns on small screens (works with grid layout only) . 153 | 'top-visible'?: 'true' | 'false'; // toggles the top bar which shows the organisations where you contributed. 154 | } 155 | 156 | declare global { 157 | namespace JSX { 158 | interface IntrinsicElements { 159 | 'widget-web-component': WidgetWebComponentProps; 160 | } 161 | } 162 | } 163 | `} 164 | language="typescript" 165 | /> 166 | { 171 | return ( 172 | <> 173 | 174 | 175 | ); 176 | } 177 | `} 178 | language="jsx" 179 | /> 180 | 181 | 182 |
183 | ); 184 | } 185 | -------------------------------------------------------------------------------- /src/app/work/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import Sidebar from '@/components/sidebar' 3 | // import InfoBar from '@/components/infobar' 4 | import { getServerSession } from 'next-auth'; 5 | import { authOptions } from '@/lib/auth'; 6 | import { redirect } from 'next/navigation'; 7 | import Sidebar from '@/components/Sidebar'; 8 | import AuxiliaryProvider from '@/components/AuxiliaryProvider'; 9 | import { Metadata } from 'next'; 10 | 11 | type Props = { children: React.ReactNode }; 12 | 13 | export const metadata: Metadata = { 14 | title: 'M&S | Dashboard', 15 | description: '', 16 | openGraph: { 17 | type: 'website', 18 | title: 'M&S | Dashboard', 19 | description: 20 | 'Showcase your open source contributions as Proof of Work by sharing your merged pull requests to anyone around the world with help of Merged&Share', 21 | images: [ 22 | { 23 | url: `${process.env.NEXT_PUBLIC_URL}/api/og-images/root`, 24 | alt: 'og-image-for-home-page', 25 | }, 26 | ], 27 | }, 28 | }; 29 | 30 | const Layout = async (props: Props) => { 31 | const session = await getServerSession(authOptions); 32 | console.log(session); 33 | if (!session || !session?.user) { 34 | redirect('/'); 35 | } 36 | 37 | return ( 38 |
39 | {/* */} 40 | 41 | {props.children} 42 |
43 | ); 44 | }; 45 | 46 | export default Layout; 47 | -------------------------------------------------------------------------------- /src/app/work/leaderboard/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import LeaderboardCard from '@/components/LeaderboardCard'; 3 | import { RefreshCcw } from 'lucide-react'; 4 | import { useSession } from 'next-auth/react'; 5 | import React, { useEffect, useState } from 'react'; 6 | 7 | export default function Leaderboard() { 8 | // const { isConnected, leaderboard, isFetching, message, requestUpdate } = useWebSocket(process.env.NEXT_PUBLIC_WS_SERVER_URL || 'ws://localhost:8080'); 9 | const session = useSession(); 10 | const currUser = session?.data?.user; 11 | const [leaderboard, setLeaderboard] = useState([]); 12 | const [isFetching, setIsFetching] = useState(false); 13 | 14 | const fetchLeaderBoard = async () => { 15 | try { 16 | setIsFetching(true); 17 | const resp = await fetch('/api/leaderboard'); 18 | const response = await resp.json(); 19 | setIsFetching(false); 20 | 21 | const leaderboardData = response?.data; 22 | setLeaderboard(leaderboardData); 23 | } catch (error) { 24 | setIsFetching(false); 25 | console.error('Error fetching leaderboard:', error); 26 | } 27 | }; 28 | 29 | useEffect(() => { 30 | fetchLeaderBoard(); 31 | }, []); 32 | 33 | return ( 34 |
35 |

Leaderboard

36 |

Below section shows you the leaderboard of the user based on the Merged&Share coin , currently you get assigned one coin on each successsful addition of your merged PR in our DB from the My-PR section (WIP)

37 | {!isFetching ? ( 38 | <> 39 | 40 | 41 | ) : ( 42 |

43 | Fetching leaderboard... 44 |

45 | )} 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/app/work/organisations/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { useState, useCallback, useEffect } from 'react'; 3 | import OrgCard from '@/components/OrgCard'; 4 | import { Input } from '@/components/ui/input'; 5 | import debounce from 'lodash/debounce'; 6 | import { Organisations as OrgType } from '@prisma/client'; 7 | import { Save, Search, SearchCode, Telescope } from 'lucide-react'; 8 | 9 | const SearchBox = ({ 10 | setOrganisations, 11 | approvedOrganisations, 12 | }: { 13 | setOrganisations: (orgs: OrgType[]) => void; 14 | approvedOrganisations: OrgType[]; 15 | }) => { 16 | const [orgName, setOrgName] = useState(''); 17 | const fetchOrganisationsOnSearch = async (name: string) => { 18 | if (name.trim() === '') { 19 | setOrganisations(approvedOrganisations); 20 | return; 21 | } 22 | 23 | const response = await fetch( 24 | `https://api.github.com/search/users?q=${name}+type:org`, 25 | { 26 | method: 'GET', 27 | headers: { 28 | Authorization: `Bearer ${process.env.NEXT_PUBLIC_GITHUB_AUTH_TOKEN}`, 29 | }, 30 | }, 31 | ); 32 | 33 | if (response.ok) { 34 | const data = await response.json(); 35 | console.log(data); 36 | 37 | const organisations: OrgType[] = data.items.map((item: any) => { 38 | const isApproved = approvedOrganisations.some( 39 | (org) => org.id === item.id, 40 | ); 41 | return { 42 | id: item.id, 43 | name: item.login, 44 | github_url: item.html_url, // Use the correct URL field 45 | avatar_url: item.avatar_url, 46 | isApproved: isApproved, 47 | }; 48 | }); 49 | 50 | // @ts-ignore 51 | setOrganisations(organisations); 52 | 53 | console.log('inside searchbox : ', { organisations }); 54 | } 55 | }; 56 | 57 | const debouncedFetchOrganisations = useCallback( 58 | debounce(fetchOrganisationsOnSearch, 400), 59 | [approvedOrganisations], 60 | ); 61 | 62 | const HandleChange = async (e: React.ChangeEvent) => { 63 | const value = e.target.value; 64 | setOrgName(value); 65 | if (value.trim() === '') { 66 | console.log('set to previous orgs : ', { approvedOrganisations }); 67 | setOrganisations(approvedOrganisations); 68 | } else { 69 | debouncedFetchOrganisations(value); 70 | } 71 | }; 72 | 73 | return ( 74 |
75 |
76 |

77 | Find your organisations 78 |

79 |

80 | Search and save the organisations which you think you have made some 81 | contributions in. 82 |

83 |
84 | 85 | 105 | 106 |
107 |
108 | 109 |
110 | 116 |
117 |
118 | ); 119 | }; 120 | 121 | const Organisations = () => { 122 | const [organisations, setOrganisations] = useState([]); 123 | const [approvedOrganisations, setApprovedOrganisations] = useState( 124 | [], 125 | ); 126 | 127 | useEffect(() => { 128 | const fetchApprovedOrganisations = async () => { 129 | try { 130 | const response = await fetch( 131 | `${process.env.NEXT_PUBLIC_URL}/api/organisation`, 132 | { 133 | method: 'GET', 134 | }, 135 | ); 136 | if (response.ok) { 137 | const data = await response.json(); 138 | 139 | const organisations: OrgType[] = data.organisations.map( 140 | (item: any) => ({ 141 | id: item.id, 142 | name: item.name, 143 | github_url: item.github_url, 144 | avatar_url: item.avatar_url, 145 | isApproved: true, 146 | }), 147 | ); 148 | 149 | setApprovedOrganisations(organisations); 150 | setOrganisations(organisations); 151 | } 152 | } catch (error) { 153 | console.log( 154 | 'Someething went wrong while fetching approved organisations at organisations page.tsx', 155 | ); 156 | } 157 | }; 158 | fetchApprovedOrganisations(); 159 | }, []); 160 | 161 | return ( 162 |
163 | 167 |
168 |
169 | {organisations && 170 | organisations.map((org) => ( 171 | 179 | ))} 180 |
181 |
182 |
183 | ); 184 | }; 185 | 186 | export default Organisations; 187 | -------------------------------------------------------------------------------- /src/components/AuxiliaryProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { ReactNode } from 'react'; 3 | import Topbar from '@/components/Topbar'; 4 | import { useSidebarStore } from '@/store/sidebar'; 5 | 6 | export default function AuxiliaryProvider({ 7 | children, 8 | }: { 9 | children: ReactNode; 10 | }) { 11 | const sidebarVisibility = useSidebarStore((state) => state.sidebarVisibility); 12 | return ( 13 |
16 | {/* */} 17 | 18 |
19 | {children} 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ContributedOrg.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Organisations } from '@prisma/client'; 3 | import Image from 'next/image'; 4 | import React from 'react'; 5 | 6 | interface ContributionType { 7 | contributions: Organisations[]; 8 | } 9 | 10 | export default function ContributedOrg({ contributions }: ContributionType) { 11 | return ( 12 |
13 |
14 | {contributions?.map((item: Organisations, index: number) => { 15 | if (index > 7) { 16 | return null; 17 | } 18 | return ( 19 | <> 20 | org 28 | 29 | ); 30 | })} 31 | {contributions?.length === 0 && 'No contributions yet'} 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/LaptopScreen.tsx: -------------------------------------------------------------------------------- 1 | import { GitMerge, Plus } from 'lucide-react'; 2 | import React from 'react'; 3 | 4 | export default function LaptopScreen() { 5 | return ( 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 | -------------------------------------------------------------------------------- /src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import Link from 'next/link'; 3 | import React from 'react'; 4 | import { useSession } from 'next-auth/react'; 5 | import { useRouter } from 'next/navigation'; 6 | import { SelectTheme } from './theme-toggler'; 7 | import { GitHubLogoIcon } from '@radix-ui/react-icons'; 8 | import { MailIcon } from 'lucide-react'; 9 | 10 | export const Navbar = () => { 11 | const session = useSession(); 12 | console.log(session?.data?.user); 13 | const router = useRouter(); 14 | 15 | return ( 16 | <> 17 |
18 |
19 | 20 |

21 | Merged&Share . 22 |

23 | 24 | 29 |
30 | 31 | 55 |
56 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/OrgCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Image from 'next/image'; 3 | import { LucideExternalLink } from 'lucide-react'; 4 | import Link from 'next/link'; 5 | import { Button } from '@/components/ui/button'; 6 | import { Organisations as OrgType } from '@prisma/client'; 7 | import GithubIcon from '../app/assets/github.svg'; 8 | 9 | import { 10 | Card, 11 | CardContent, 12 | CardFooter, 13 | CardHeader, 14 | CardTitle, 15 | } from '@/components/ui/card'; 16 | import { Switch } from './ui/switch'; 17 | import { GitHubLogoIcon } from '@radix-ui/react-icons'; 18 | 19 | export default function OrgCard({ 20 | organisation, 21 | isApproved, 22 | setApprovedOrganisations, 23 | setOrganisations, 24 | }: { 25 | organisation: OrgType; 26 | isApproved: boolean; 27 | setApprovedOrganisations: (orgs: OrgType[]) => void; 28 | setOrganisations: (orgs: OrgType[]) => void; 29 | }) { 30 | const login = 31 | organisation.name.length > 10 32 | ? `${organisation.name.slice(0, 20)}...` 33 | : organisation.name; 34 | 35 | const [approved, setApproved] = useState(isApproved); 36 | 37 | const handleApproval = async () => { 38 | const newApprovedState = !approved; 39 | setApproved(newApprovedState); 40 | 41 | try { 42 | const response = await fetch( 43 | `${process.env.NEXT_PUBLIC_URL}/api/organisation`, 44 | { 45 | method: 'POST', 46 | body: JSON.stringify({ 47 | id: organisation.id, 48 | name: organisation.name, 49 | github_url: organisation.github_url, 50 | avatar_url: organisation.avatar_url, 51 | }), 52 | }, 53 | ); 54 | const finalData = await response.json(); 55 | 56 | if (finalData.action === 'approved') { 57 | setApproved(true); 58 | // @ts-ignore 59 | setApprovedOrganisations((previousOrgs) => [ 60 | ...previousOrgs, 61 | organisation, 62 | ]); 63 | } else if (finalData.action === 'disapproved') { 64 | setApproved(!newApprovedState); 65 | // @ts-ignore 66 | setApprovedOrganisations((previousOrgs) => 67 | // @ts-ignore 68 | previousOrgs.filter((org) => org.id !== organisation.id), 69 | ); 70 | // @ts-ignore 71 | setOrganisations((previousOrgs) => 72 | // @ts-ignore 73 | previousOrgs.filter((org) => org.id !== organisation.id), 74 | ); 75 | } 76 | } catch (error) { 77 | console.error('Error updating approval status:', error); 78 | setApproved(!newApprovedState); 79 | } 80 | }; 81 | 82 | return ( 83 | <> 84 |
85 |
86 | {`${organisation.name} 93 |
94 | 95 |
96 |

{login}

97 |
98 | 99 |
100 | 105 | 106 | 107 | 108 | 113 |
114 |
115 | 116 | {/* 117 | 118 | {login} 119 | 120 | 121 | Image not found 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | */} 138 | 139 | ); 140 | } 141 | -------------------------------------------------------------------------------- /src/components/PRListings.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { useState } from 'react'; 3 | import { 4 | Select, 5 | SelectContent, 6 | SelectGroup, 7 | SelectItem, 8 | SelectLabel, 9 | SelectTrigger, 10 | SelectValue, 11 | } from '@/components/ui/select'; 12 | import { Organisations } from '@prisma/client'; 13 | import Image from 'next/image'; 14 | import PRCard from './PRCard'; 15 | import { Badge } from './ui/badge'; 16 | 17 | export default function PRListings({ 18 | urlUser, 19 | user, 20 | pullRequests, 21 | organisationData, 22 | }: any) { 23 | const [selectedOrg, setSelectedOrg] = useState(''); 24 | const [isOrgAndPRMatched, setIsOrgAndPRMatched] = useState(false); 25 | 26 | const checkIfAnyPRMatches = () => { 27 | return urlUser?.pullRequests?.some( 28 | (item: any) => item.org.id === selectedOrg, 29 | ); 30 | }; 31 | 32 | const filteredPullRequests = selectedOrg 33 | ? urlUser?.pullRequests?.filter( 34 | (item: any) => item?.org?.id === selectedOrg, 35 | ) 36 | : urlUser?.pullRequests; 37 | 38 | return ( 39 |
40 |
41 |
42 | 43 | All PRs 44 | 45 |
46 | {/* @ts-ignore */} 47 | 79 |
80 | 81 |
82 | {selectedOrg && !checkIfAnyPRMatches() && ( 83 |
84 | No pull requests found for the selected organization. 85 |
86 | )} 87 | {filteredPullRequests?.map((item: any, index: number) => { 88 | return ( 89 |
90 | 96 |
97 | ); 98 | })} 99 | 100 | {/*
{!isOrgAndPRMatched && <>Not found}
*/} 101 |
102 |
103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /src/components/PersonalDetailsForm.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanbairagi30/merged-n-share/96b785f6b23f810a706373349c75d34257d0537c/src/components/PersonalDetailsForm.tsx -------------------------------------------------------------------------------- /src/components/ProfileEditForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { Button } from '@/components/ui/button'; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogDescription, 8 | DialogHeader, 9 | DialogTitle, 10 | DialogTrigger, 11 | } from '@/components/ui/dialog'; 12 | import { Input } from '@/components/ui/input'; 13 | import { Pencil } from 'lucide-react'; 14 | import { toast } from 'sonner'; 15 | 16 | export default function ProfileEditForm({ user }: any) { 17 | const [newName, setNewName] = useState(''); 18 | const [loading, setLoading] = useState(false); 19 | const [openDialog, setOpenDialog] = useState(false); 20 | const handleEditUpdate = async () => { 21 | setLoading(true); 22 | if (!newName) { 23 | toast.warning(' it must be a vaid name'); 24 | return; 25 | } 26 | const resp = await fetch(`${process.env.NEXT_PUBLIC_URL}/api/user`, { 27 | method: 'PUT', 28 | body: JSON.stringify({ name: newName.trim() }), 29 | }); 30 | 31 | const response = await resp.json(); 32 | console.log(response); 33 | if (response?.success) { 34 | toast.success(response?.message); 35 | } 36 | setLoading(false); 37 | setOpenDialog(false); 38 | }; 39 | return ( 40 |
41 | 42 | setOpenDialog(true)}> 43 |
44 |

Edit

45 | 46 |
47 |
48 | 49 | 50 | 51 | Edit 52 | 53 |
54 | setNewName(e.target.value)} 56 | className="focus:border-2" 57 | placeholder={`${user?.name}`} 58 | /> 59 | 60 |
61 |
62 |
63 |
64 | ); 65 | } 66 | 67 | export const PersonalDetailForm = () => { 68 | const [socialLinks, setSocialLinks] = useState({ x: '', linkedIn: '' }); 69 | 70 | const updateLinks = async () => { 71 | const response = await fetch(`${process.env.NEXT_PUBLIC_URL}/api/user`, { 72 | method: 'PUT', 73 | body: JSON.stringify({ socialLinks }), 74 | }); 75 | 76 | const finalResponse = await response.json(); 77 | if (finalResponse.success) { 78 | toast.success('Links Updated Successfully'); 79 | } else { 80 | toast.error('Something went wron , Please try again after sometime'); 81 | } 82 | }; 83 | 84 | const getUserDetails = async () => { 85 | const response = await fetch(`${process.env.NEXT_PUBLIC_URL}/api/user`, { 86 | method: 'GET', 87 | }); 88 | 89 | const resp = await response.json(); 90 | 91 | console.log(resp?.user); 92 | setSocialLinks({ 93 | x: resp?.user[0]?.xProfile, 94 | linkedIn: resp?.user[0]?.linkedInProfile, 95 | }); 96 | }; 97 | 98 | useEffect(() => { 99 | getUserDetails(); 100 | }, []); 101 | 102 | return ( 103 |
104 |
105 |

X profile (optional)

106 | 111 | setSocialLinks((prev) => ({ ...prev, x: e.target.value.trim() })) 112 | } 113 | /> 114 |
115 |
116 |

LinkedIn profile (optional)

117 | 122 | setSocialLinks((prev) => ({ 123 | ...prev, 124 | linkedIn: e.target.value.trim(), 125 | })) 126 | } 127 | /> 128 |
129 | 130 | 136 |
137 | ); 138 | }; 139 | -------------------------------------------------------------------------------- /src/components/ProfileSwitch.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { Switch } from './ui/switch'; 4 | 5 | export default function ProfileSwitch() { 6 | const [profileVisibility, setProfileVisibility] = useState(false); 7 | 8 | const getUserData = async () => { 9 | const response = await fetch(`${process.env.NEXT_PUBLIC_URL}/api/user`, { 10 | method: 'GET', 11 | }); 12 | 13 | const resp = await response.json(); 14 | 15 | console.log(resp?.user); 16 | setProfileVisibility(resp?.user[0]?.isProfilePublic); 17 | }; 18 | useEffect(() => { 19 | getUserData(); 20 | }, []); 21 | 22 | const updatedUserProfile = async (e: boolean) => { 23 | try { 24 | setProfileVisibility(e); 25 | const response = await fetch(`${process.env.NEXT_PUBLIC_URL}/api/user`, { 26 | method: 'PUT', 27 | body: JSON.stringify({ isChecked: e }), 28 | }); 29 | const data = await response.json(); 30 | if (!data.success) { 31 | console.error('Failed to update profile:', data.message); 32 | setProfileVisibility(!e); // Revert the local state if the API call fails 33 | } 34 | } catch (error) { 35 | console.error('Error updating profile:', error); 36 | setProfileVisibility(!e); // Revert the local state if there's an error 37 | } 38 | }; 39 | return ( 40 |
41 | {/*

Visibility

*/} 42 | updatedUserProfile(e)} 45 | defaultChecked 46 | className="data-[state=checked]:bg-primary" 47 | id="airplane-mode" 48 | /> 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/components/Providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { SessionProvider } from 'next-auth/react'; 3 | import React, { ReactNode } from 'react'; 4 | 5 | interface Props { 6 | children: ReactNode; 7 | } 8 | 9 | const Providers = (props: Props) => { 10 | return {props.children}; 11 | }; 12 | 13 | export default Providers; 14 | -------------------------------------------------------------------------------- /src/components/RequestAccessButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React from 'react'; 3 | import { toast } from 'sonner'; 4 | 5 | export default function RequestAccessButton() { 6 | return ( 7 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { sideBarOptions } from '@/data/data'; 3 | import { X } from 'lucide-react'; 4 | import { useSession } from 'next-auth/react'; 5 | import Image from 'next/image'; 6 | import { usePathname, useRouter } from 'next/navigation'; 7 | import React, { useEffect, useState } from 'react'; 8 | import Link from 'next/link'; 9 | import { useSidebarStore } from '@/store/sidebar'; 10 | import { GitHubLogoIcon } from '@radix-ui/react-icons'; 11 | import { Button } from './ui/button'; 12 | import { Badge } from './ui/badge'; 13 | 14 | export default function Sidebar() { 15 | const pathName = usePathname(); 16 | const [activeIndex, setActiveIndex] = useState(() => { 17 | return pathName === '/work/dashboard' ? 0 : null; 18 | }); 19 | const router = useRouter(); 20 | const session = useSession(); 21 | const user = session?.data?.user; 22 | 23 | const isProfile = pathName === '/work/profile'; 24 | 25 | const sidebarVisibility = useSidebarStore((state) => state.sidebarVisibility); 26 | const toggleSidebar = useSidebarStore( 27 | (state) => state.toggleSidebarVisibility, 28 | ); 29 | 30 | useEffect(() => { 31 | const currentPathName = pathName; 32 | const newActive = sideBarOptions.general.findIndex( 33 | (option: any) => option.href === currentPathName, 34 | ); 35 | setActiveIndex(newActive); 36 | }, [pathName]); 37 | 38 | return ( 39 | <> 40 | {sidebarVisibility && ( 41 |
toggleSidebar(false)} 43 | className="absolute z-50 block h-screen w-full bg-background/90 bg-opacity-10 md:hidden" 44 | >
45 | )} 46 |
53 |
54 | 55 |

56 | M&S 57 |

58 | 59 | 63 | 64 | 65 |
66 | 67 | 73 | 74 | {/*
*/} 75 |
76 | GENERAL 77 |
78 | 79 |
80 |
81 | {sideBarOptions.general.map((x, idx) => { 82 | if (x.name.toLowerCase() === 'organisation') { 83 | // @ts-ignore 84 | // if (!user?.admin && user?.username !== 'hkirat') { 85 | // return null; 86 | // } 87 | } 88 | 89 | return ( 90 | toggleSidebar(false)} 92 | href={`${x.href}`} 93 | key={idx} 94 | > 95 |
setActiveIndex(idx)} 97 | className={`flex cursor-pointer items-center text-sm ${activeIndex === idx ? 'border-r-[5px] border-primary bg-primary/20' : 'hover:bg-accent'} h-fit gap-1 rounded-l-md px-2 py-2`} 98 | > 99 |
102 | 103 |
104 |
105 |

{x.name}

106 | {x.isNew && ( 107 | 108 | NEW 109 | 110 | )} 111 |
112 |
113 | 114 | ); 115 | })} 116 |
117 | 118 | 142 |
143 |
144 | 145 | ); 146 | } 147 | -------------------------------------------------------------------------------- /src/components/TopLeaderCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function TopLeaderCard() { 4 | return
Top
; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Topbar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Bell, LogOut } from 'lucide-react'; 3 | import React, { useState } from 'react'; 4 | import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'; 5 | import { Button } from './ui/button'; 6 | import { signOut } from 'next-auth/react'; 7 | import { HamburgerMenuIcon } from '@radix-ui/react-icons'; 8 | import { useSidebarStore } from '@/store/sidebar'; 9 | import { usePathname } from 'next/navigation'; 10 | import { capitalizeFirstLetter } from '@/util'; 11 | import { SelectTheme } from './theme-toggler'; 12 | 13 | export default function Topbar() { 14 | const [openDialog, setOpenDialog] = useState(false); 15 | const sidebarVisibility = useSidebarStore((state) => state.sidebarVisibility); 16 | const toggleSidebar = useSidebarStore( 17 | (state) => state.toggleSidebarVisibility, 18 | ); 19 | 20 | const pathName = usePathname(); 21 | return ( 22 |
23 | toggleSidebar(true)} 25 | className="mr-4 block cursor-pointer md:hidden" 26 | /> 27 |
28 | 29 | {capitalizeFirstLetter(pathName.split('/')[2])} 30 | 31 |
32 |
33 | 34 | 35 | 36 | setOpenDialog(true)}> 37 | 38 | 39 | 40 | 41 | 42 | Do you want to logout ? 43 | 44 |
45 | 52 | 59 |
60 |
61 |
62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/WallOfLove.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React from 'react'; 3 | import TitleCard from './title-card'; 4 | import { MessageCircle } from 'lucide-react'; 5 | import { useTheme } from 'next-themes'; 6 | 7 | const WallOfLove = () => { 8 | const { theme } = useTheme(); 9 | 10 | const textColor = theme === 'dark' ? 'FFFFFF' : '000000'; 11 | 12 | return ( 13 |
14 | 15 | 16 |
17 |

18 | 19 | People are loving Merged 20 |

&

Share 21 |
22 | out there. 23 |

24 |
25 | 31 |
32 | ); 33 | }; 34 | 35 | export default WallOfLove; 36 | -------------------------------------------------------------------------------- /src/components/alert-box.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | 3 | interface AlertBoxProps { 4 | icon: ReactNode; 5 | title: string; 6 | message: ReactNode; 7 | } 8 | 9 | export function AlertBox({ icon, title, message }: AlertBoxProps) { 10 | return ( 11 |
12 |
13 | {icon} 14 |
15 |

{title}

16 |

{message}

17 |
18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/code-block.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import SyntaxHighlighter from 'react-syntax-highlighter'; 3 | import { atomOneDark } from 'react-syntax-highlighter/dist/esm/styles/hljs'; 4 | import { Button } from '@/components/ui/button'; 5 | import { Check, Copy } from 'lucide-react'; 6 | 7 | interface CodeBlockProps { 8 | code: string; 9 | language: string; 10 | } 11 | 12 | export function CodeBlock({ code, language }: CodeBlockProps) { 13 | const [isCopied, setIsCopied] = useState(false); 14 | 15 | const handleCopy = async () => { 16 | await navigator.clipboard.writeText(code); 17 | setIsCopied(true); 18 | setTimeout(() => setIsCopied(false), 2000); 19 | }; 20 | 21 | return ( 22 |
23 | 34 | {code} 35 | 36 | 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/components/desktop-screen.tsx: -------------------------------------------------------------------------------- 1 | import Safari from '@/components/ui/safari'; 2 | 3 | export function DesktopScreen() { 4 | return ( 5 |
6 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/fixed-marketing-navbar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React from 'react'; 3 | 4 | export default function MarketingNavbar() { 5 | return ( 6 |
7 | Powered by{' '} 8 |

window.open(process.env.NEXT_PUBLIC_URL, '_blank')} 10 | className="cursor-pointer font-secondary text-lg font-extrabold hover:underline" 11 | > 12 | {' '} 13 | Merged&Share. 14 |

15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import { GitHubLogoIcon, TwitterLogoIcon } from '@radix-ui/react-icons'; 2 | import { ArrowUpRight } from 'lucide-react'; 3 | import React from 'react'; 4 | import X from './svgs/x'; 5 | import Link from 'next/link'; 6 | 7 | export default function Footer() { 8 | const year = new Date().getFullYear(); 9 | return ( 10 |
11 |
12 | 20 | 21 | 38 |
39 | 40 |
41 | 42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/components/frameworks-list.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Badge } from '@/components/ui/badge'; 3 | import { Info } from 'lucide-react'; 4 | 5 | interface Framework { 6 | name: string; 7 | icon: React.ComponentType<{ className?: string }>; 8 | } 9 | 10 | interface FrameworkListProps { 11 | frameworks: Framework[]; 12 | } 13 | 14 | export function FrameworkList({ frameworks }: FrameworkListProps) { 15 | return ( 16 |
17 | 18 |
19 |

20 | We support multiple frameworks where you can embed your PRs, which are 21 | listed below 22 |

23 |
24 | {frameworks.map((tool, index) => ( 25 | 29 | {tool.name} 30 | 31 | 32 | ))} 33 |
34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/google-analytics.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Script from 'next/script'; 3 | 4 | const GoogleAnalytics = () => { 5 | return ( 6 | <> 7 | 22 | 23 | ); 24 | }; 25 | 26 | export default GoogleAnalytics; 27 | -------------------------------------------------------------------------------- /src/components/hero-buttons.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React from 'react'; 3 | import { Button } from './ui/button'; 4 | import { signIn, useSession } from 'next-auth/react'; 5 | import Image from 'next/image'; 6 | import { useRouter } from 'next/navigation'; 7 | 8 | export default function HeroButtons() { 9 | const session = useSession(); 10 | const user = session?.data?.user; 11 | const router = useRouter(); 12 | 13 | const handlePrimaryButtonClick = async () => { 14 | if (!user) { 15 | await signIn(); 16 | } 17 | router.push('/work/dashboard'); 18 | }; 19 | 20 | return ( 21 | <> 22 | 41 | 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/components/home-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React from 'react'; 3 | import { Button } from './ui/button'; 4 | import { useRouter } from 'next/navigation'; 5 | 6 | export default function HomeButton() { 7 | const router = useRouter(); 8 | return ( 9 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/components/how-it-works.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | Building, 5 | FileText, 6 | GitPullRequest, 7 | Share, 8 | Share2, 9 | Upload, 10 | UserCheck, 11 | UserPlus, 12 | } from 'lucide-react'; 13 | import { motion } from 'framer-motion'; 14 | import { useInView } from 'framer-motion'; 15 | import { useRef, useState } from 'react'; 16 | 17 | const TimelineCard = ({ step, index, inView }: any) => { 18 | const [isHovered, setIsHovered] = useState(false); 19 | 20 | return ( 21 | 27 | {/* Timeline dot with ping animation */} 28 |
29 |
30 |
31 |
32 | 33 | {/* Content */} 34 |
setIsHovered(true)} 41 | onMouseLeave={() => setIsHovered(false)} 42 | > 43 |
44 | {/* Background gradient effect */} 45 |
49 | 50 | {/* Icon with gradient background */} 51 |
52 |
53 |
54 |
55 | {step.icon} 56 |
57 | {/* Number indicator */} 58 |
59 | 60 | {index + 1} 61 | 62 |
63 |
64 | 65 |
66 |

67 | {step.title} 68 |

69 |

70 | {step.description} 71 |

72 |
73 |
74 | 75 | {/* Decorative elements */} 76 |
77 |
78 |
79 |
80 | 81 | ); 82 | }; 83 | 84 | const HowItWorks = () => { 85 | const containerRef = useRef(null); 86 | const isInView = useInView(containerRef, { once: true, margin: '-100px' }); 87 | const steps = [ 88 | { 89 | icon: , 90 | title: 'Sign Up and Access Dashboard', 91 | description: 92 | 'Create an account to get started and access your personalized dashboard.', 93 | }, 94 | { 95 | icon: , 96 | title: 'Add Organizations', 97 | description: 98 | 'Select organizations where you’ve contributed or plan to contribute.', 99 | }, 100 | { 101 | icon: , 102 | title: 'Fetch and Save PRs', 103 | description: 104 | 'Fetch merged PRs from selected organizations and save them to your profile.', 105 | }, 106 | { 107 | icon: , 108 | title: 'Share and Embed PRs', 109 | description: 110 | 'Showcase your contributions via public profile or embed them on websites.', 111 | }, 112 | ]; 113 | 114 | return ( 115 |
116 |
117 | 123 |

124 | How It Works 125 |

126 |

127 | Track and showcase your open-source contributions effortlessly, from 128 | adding organizations to embedding PRs. 129 |

130 |
131 | 132 |
133 | {/* Vertical Line with gradient */} 134 |
135 | 136 |
137 | {steps.map((step, index) => ( 138 | 144 | ))} 145 |
146 |
147 |
148 |
149 | ); 150 | }; 151 | 152 | export default HowItWorks; 153 | -------------------------------------------------------------------------------- /src/components/pr-delete-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState } from 'react'; 4 | import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'; 5 | import { Button } from './ui/button'; 6 | import { Trash2, Loader2 } from 'lucide-react'; 7 | import { usePathname } from 'next/navigation'; 8 | import { toast } from 'sonner'; 9 | 10 | export default function PrDeleteButton({ 11 | pullRequestId, 12 | getAllPullrequests, 13 | }: { 14 | pullRequestId: string; 15 | getAllPullrequests: () => void; 16 | }) { 17 | const [openDialog, setOpenDialog] = useState(false); 18 | const [isDeleting, setIsDeleting] = useState(false); 19 | const pathname = usePathname(); 20 | const isMyPrSection = pathname === '/work/my-pr'; 21 | 22 | const handleDeletePR = async () => { 23 | setIsDeleting(true); 24 | try { 25 | const response = await fetch('/api/pr', { 26 | method: 'DELETE', 27 | headers: { 28 | 'Content-Type': 'application/json', 29 | }, 30 | body: JSON.stringify({ prId: pullRequestId }), 31 | }); 32 | 33 | const data = await response.json(); 34 | 35 | if (data.success) { 36 | toast.success(data.message); 37 | getAllPullrequests(); 38 | setOpenDialog(false); 39 | } else { 40 | toast.error(data.message || 'Something went wrong'); 41 | } 42 | } catch (error) { 43 | toast.error('An error occurred while deleting the pull request'); 44 | } finally { 45 | setIsDeleting(false); 46 | } 47 | }; 48 | 49 | if (!isMyPrSection) return null; 50 | 51 | return ( 52 | 53 | 54 | 57 | 58 | 59 | 60 | 61 | 62 | Do you really want to delete this pull request? 63 | 64 | 65 |
66 | 81 | 89 |
90 |
91 |
92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/components/preview-widget.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ScanEye } from 'lucide-react'; 3 | import { 4 | Sheet, 5 | SheetContent, 6 | SheetDescription, 7 | SheetHeader, 8 | SheetTitle, 9 | SheetTrigger, 10 | } from '@/components/ui/sheet'; 11 | 12 | interface PreviewWidgetProps { 13 | theme: string | undefined; 14 | username: string | undefined; 15 | } 16 | 17 | export function PreviewWidget({ theme, username }: PreviewWidgetProps) { 18 | return ( 19 |
20 |
21 | 22 |
23 |

Preview the Code

24 |
25 | 26 | 27 |
28 | See Preview 29 |
30 |
31 | 32 | 33 | 34 | You are currently previewing the PR widget 35 | 36 | 37 | Below shows that how your PR widget will look like when it 38 | is on your own website 39 | 40 | 41 |
42 | 48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/components/product-demo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Image from 'next/image'; 3 | import dashboard from '../../public/dashboard.png'; 4 | 5 | export default function ProductDemo() { 6 | return ( 7 |
8 | dashboard 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/profile-view.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useRef } from 'react'; 4 | 5 | interface ProfileViewProps { 6 | userId: string; 7 | currentUserId: string | undefined; 8 | } 9 | 10 | export default function ProfileView({ 11 | userId, 12 | currentUserId, 13 | }: ProfileViewProps) { 14 | const hasRecordedView = useRef(false); 15 | 16 | useEffect(() => { 17 | const recordView = async () => { 18 | if (hasRecordedView.current || userId === currentUserId) return; 19 | 20 | try { 21 | const response = await fetch('/api/analytics', { 22 | method: 'POST', 23 | headers: { 24 | 'Content-Type': 'application/json', 25 | }, 26 | body: JSON.stringify({ userId }), 27 | }); 28 | 29 | if (!response.ok) { 30 | throw new Error('Failed to record view'); 31 | } 32 | 33 | const data = await response.json(); 34 | console.log(data.message); 35 | hasRecordedView.current = true; 36 | } catch (error) { 37 | console.error('Error recording view:', error); 38 | } 39 | }; 40 | 41 | recordView(); 42 | }, [userId, currentUserId]); 43 | 44 | return null; // This component doesn't render anything 45 | } 46 | -------------------------------------------------------------------------------- /src/components/profile-views-chart.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | Area, 5 | AreaChart, 6 | Bar, 7 | Tooltip, 8 | CartesianGrid, 9 | XAxis, 10 | YAxis, 11 | } from 'recharts'; 12 | 13 | import { 14 | ChartConfig, 15 | ChartContainer, 16 | ChartLegend, 17 | ChartLegendContent, 18 | ChartTooltip, 19 | ChartTooltipContent, 20 | } from '@/components/ui/chart'; 21 | import { useEffect, useState } from 'react'; 22 | import { Button } from './ui/button'; 23 | 24 | const chartData = [ 25 | { month: 'January', desktop: 186 }, 26 | { month: 'February', desktop: 305 }, 27 | { month: 'March', desktop: 237 }, 28 | { month: 'April', desktop: 73 }, 29 | { month: 'May', desktop: 209 }, 30 | { month: 'June', desktop: 214 }, 31 | ]; 32 | const chartConfig = { 33 | views: { 34 | label: 'Views', 35 | color: '#facc15', 36 | }, 37 | } satisfies ChartConfig; 38 | 39 | export function ProfileChart() { 40 | const [viewsData, setViewsData] = useState([]); 41 | const [range, setRange] = useState<'24h' | '7d' | '30d'>('7d'); 42 | 43 | useEffect(() => { 44 | const fetchViewsData = async () => { 45 | try { 46 | const response = await fetch( 47 | `${process.env.NEXT_PUBLIC_URL}/api/analytics?range=${range}`, 48 | ); 49 | if (!response.ok) throw new Error('Failed to fetch data'); 50 | const data = await response.json(); 51 | setViewsData(data); 52 | } catch (error) { 53 | console.error('Error fetching profile views:', error); 54 | } 55 | }; 56 | 57 | fetchViewsData(); 58 | }, [range]); 59 | 60 | const formatXAxis = (tickItem: string) => { 61 | const date = new Date(tickItem); 62 | return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); 63 | }; 64 | 65 | return ( 66 | <> 67 |
68 | 74 | 80 | 86 |
87 | 88 | 92 | 93 | 94 | 100 | } /> 101 | 108 | 109 | 110 | 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /src/components/statistics.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { 3 | Eye, 4 | MessageCircleHeart, 5 | Telescope, 6 | TrendingUp, 7 | Users, 8 | } from 'lucide-react'; 9 | import React from 'react'; 10 | import TitleCard from './title-card'; 11 | import { motion } from 'framer-motion'; 12 | 13 | const statisticsData = [ 14 | { icon: Eye, value: '4000+', label: 'Visitors' }, 15 | { icon: Users, value: '200+', label: 'Users' }, 16 | { icon: Telescope, value: '3500+', label: 'Page Views' }, 17 | { icon: MessageCircleHeart, value: '10+', label: 'Testimonials' }, 18 | ]; 19 | 20 | export default function Statistics() { 21 | const containerVariants = { 22 | hidden: { opacity: 0 }, 23 | visible: { 24 | opacity: 1, 25 | transition: { 26 | staggerChildren: 0.2, 27 | }, 28 | }, 29 | }; 30 | 31 | const itemVariants = { 32 | hidden: { opacity: 0, y: 20 }, 33 | visible: { 34 | opacity: 1, 35 | y: 0, 36 | transition: { 37 | duration: 0.5, 38 | ease: 'easeOut', 39 | }, 40 | }, 41 | }; 42 | 43 | return ( 44 | 51 | 52 | 53 | 54 | 55 | 59 |

60 | Let's see some numbers{' '} 61 | which we've got so far. 62 |

63 |
64 | 65 | 69 | {statisticsData.map((stat, index) => ( 70 | 75 | 76 |
77 | 83 | {stat.value} 84 | 85 |
86 | {stat.label} 87 |
88 |
89 |
90 | ))} 91 |
92 | 93 | 94 | and counting... 95 | 96 |
97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /src/components/svgs/html.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | interface SvgIconProps { 3 | className?: string; 4 | } 5 | 6 | const HTML: React.FC = ({ className }) => { 7 | return ( 8 | 17 | 18 | 19 | 23 | 27 | 28 | ); 29 | }; 30 | 31 | export default HTML; 32 | -------------------------------------------------------------------------------- /src/components/svgs/next-js.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface SvgIconProps { 4 | className?: string; 5 | } 6 | 7 | const NextJs: React.FC = ({ className }) => { 8 | return ( 9 | 18 | 19 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default NextJs; 37 | -------------------------------------------------------------------------------- /src/components/svgs/react-js.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface SvgIconProps { 4 | className?: string; 5 | } 6 | 7 | const ReactJS: React.FC = ({ className }) => { 8 | return ( 9 | 18 | 22 | 26 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default ReactJS; 36 | -------------------------------------------------------------------------------- /src/components/svgs/x.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const X = ({ className }: { className: string }) => { 4 | return ( 5 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default X; 18 | -------------------------------------------------------------------------------- /src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { ThemeProvider as NextThemesProvider } from 'next-themes'; 5 | import { type ThemeProviderProps } from 'next-themes/dist/types'; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/theme-toggler.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { MoonIcon, SunIcon } from 'lucide-react'; 5 | import { useTheme } from 'next-themes'; 6 | 7 | export function SelectTheme() { 8 | const { setTheme, theme } = useTheme(); 9 | return ( 10 | <> 11 |
setTheme(theme === 'light' ? 'dark' : 'light')} 14 | > 15 | 16 | 17 |
18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/title-card.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function TitleCard({ icon: Icon, title }: any) { 4 | return ( 5 |
6 |
7 | 8 |

{title}

9 |
10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/components/ui/avatar-circle.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | 'use client'; 3 | 4 | import React from 'react'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | type Avatar = { 9 | img: string; 10 | href: string; 11 | }; 12 | 13 | interface AvatarCirclesProps { 14 | className?: string; 15 | numPeople?: number; 16 | avatarUrls: Avatar[]; 17 | } 18 | 19 | const AvatarCircles = ({ 20 | numPeople, 21 | className, 22 | avatarUrls, 23 | }: AvatarCirclesProps) => { 24 | return ( 25 |
26 | {avatarUrls.map((item, index) => ( 27 | window.open(item.href, '_blank')} 33 | height={40} 34 | alt={`Avatar ${index + 1}`} 35 | /> 36 | ))} 37 | 38 | +{numPeople} 39 | 40 |
41 | ); 42 | }; 43 | 44 | export default AvatarCircles; 45 | -------------------------------------------------------------------------------- /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/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { cva, type VariantProps } from 'class-variance-authority'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | const badgeVariants = cva( 7 | 'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 8 | { 9 | variants: { 10 | variant: { 11 | default: 'border-transparent bg-primary text-primary-foreground shadow', 12 | secondary: 13 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 14 | destructive: 15 | 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80', 16 | outline: 'text-foreground', 17 | }, 18 | }, 19 | defaultVariants: { 20 | variant: 'default', 21 | }, 22 | }, 23 | ); 24 | 25 | export interface BadgeProps 26 | extends React.HTMLAttributes, 27 | VariantProps {} 28 | 29 | function Badge({ className, variant, ...props }: BadgeProps) { 30 | return ( 31 |
32 | ); 33 | } 34 | 35 | export { Badge, badgeVariants }; 36 | -------------------------------------------------------------------------------- /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 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', 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-8 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 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

41 | )); 42 | CardTitle.displayName = 'CardTitle'; 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 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 { Cross2Icon } from '@radix-ui/react-icons'; 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 |
81 | ); 82 | DialogFooter.displayName = 'DialogFooter'; 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )); 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )); 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogTrigger, 116 | DialogClose, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | }; 123 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = 'Input'; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { 5 | CaretSortIcon, 6 | CheckIcon, 7 | ChevronDownIcon, 8 | ChevronUpIcon, 9 | } from '@radix-ui/react-icons'; 10 | import * as SelectPrimitive from '@radix-ui/react-select'; 11 | 12 | import { cn } from '@/lib/utils'; 13 | 14 | const Select = SelectPrimitive.Root; 15 | 16 | const SelectGroup = SelectPrimitive.Group; 17 | 18 | const SelectValue = SelectPrimitive.Value; 19 | 20 | const SelectTrigger = React.forwardRef< 21 | React.ElementRef, 22 | React.ComponentPropsWithoutRef 23 | >(({ className, children, ...props }, ref) => ( 24 | span]:line-clamp-1', 28 | className, 29 | )} 30 | {...props} 31 | > 32 | {children} 33 | 34 | 35 | 36 | 37 | )); 38 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; 39 | 40 | const SelectScrollUpButton = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | 53 | 54 | )); 55 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; 56 | 57 | const SelectScrollDownButton = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, ...props }, ref) => ( 61 | 69 | 70 | 71 | )); 72 | SelectScrollDownButton.displayName = 73 | SelectPrimitive.ScrollDownButton.displayName; 74 | 75 | const SelectContent = React.forwardRef< 76 | React.ElementRef, 77 | React.ComponentPropsWithoutRef 78 | >(({ className, children, position = 'popper', ...props }, ref) => ( 79 | 80 | 91 | 92 | 99 | {children} 100 | 101 | 102 | 103 | 104 | )); 105 | SelectContent.displayName = SelectPrimitive.Content.displayName; 106 | 107 | const SelectLabel = React.forwardRef< 108 | React.ElementRef, 109 | React.ComponentPropsWithoutRef 110 | >(({ className, ...props }, ref) => ( 111 | 116 | )); 117 | SelectLabel.displayName = SelectPrimitive.Label.displayName; 118 | 119 | const SelectItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )); 139 | SelectItem.displayName = SelectPrimitive.Item.displayName; 140 | 141 | const SelectSeparator = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef 144 | >(({ className, ...props }, ref) => ( 145 | 150 | )); 151 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName; 152 | 153 | export { 154 | Select, 155 | SelectGroup, 156 | SelectValue, 157 | SelectTrigger, 158 | SelectContent, 159 | SelectLabel, 160 | SelectItem, 161 | SelectSeparator, 162 | SelectScrollUpButton, 163 | SelectScrollDownButton, 164 | }; 165 | -------------------------------------------------------------------------------- /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 { Cross2Icon } from '@radix-ui/react-icons'; 6 | import { cva, type VariantProps } from 'class-variance-authority'; 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 | 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-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-full md:w-[90%] border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-full', 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/sonner.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useTheme } from 'next-themes'; 4 | import { Toaster as Sonner } from 'sonner'; 5 | 6 | type ToasterProps = React.ComponentProps; 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = 'system' } = useTheme(); 10 | 11 | return ( 12 | 28 | ); 29 | }; 30 | 31 | export { Toaster }; 32 | -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as SwitchPrimitives from '@radix-ui/react-switch'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | const Switch = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 25 | 26 | )); 27 | Switch.displayName = SwitchPrimitives.Root.displayName; 28 | 29 | export { Switch }; 30 | -------------------------------------------------------------------------------- /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/tooltip.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as TooltipPrimitive from '@radix-ui/react-tooltip'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider; 9 | 10 | const Tooltip = TooltipPrimitive.Root; 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger; 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 27 | )); 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 29 | 30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 31 | -------------------------------------------------------------------------------- /src/components/users-avatars.tsx: -------------------------------------------------------------------------------- 1 | import AvatarCircles from './ui/avatar-circle'; 2 | 3 | const avatarUrls = [ 4 | { 5 | img: 'https://avatars.githubusercontent.com/u/8079861', 6 | href: `${process.env.NEXT_PUBLIC_URL}/profile/hkirat`, 7 | }, 8 | { 9 | img: 'https://avatars.githubusercontent.com/u/76874341', 10 | href: `${process.env.NEXT_PUBLIC_URL}/profile/devsargam`, 11 | }, 12 | { 13 | img: 'https://avatars.githubusercontent.com/u/37402791', 14 | href: `${process.env.NEXT_PUBLIC_URL}/profile/nimit9`, 15 | }, 16 | { 17 | img: 'https://avatars.githubusercontent.com/u/89733575', 18 | href: `${process.env.NEXT_PUBLIC_URL}/profile/TanmayDhobale`, 19 | }, 20 | ]; 21 | 22 | export async function UsersAvatar() { 23 | return ; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/your-profile-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React from 'react'; 3 | import { Badge } from './ui/badge'; 4 | import { LucideExternalLink } from 'lucide-react'; 5 | 6 | export default function YourProfileButton({ 7 | username, 8 | }: { 9 | username: string | undefined; 10 | }) { 11 | return ( 12 | window.open(`/profile/${username}`, '_blank')} 14 | className="flex cursor-pointer items-center gap-2 bg-accent py-1 text-foreground hover:bg-accent" 15 | > 16 | Your profile 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/data/data.ts: -------------------------------------------------------------------------------- 1 | import HTML from '@/components/svgs/html'; 2 | import NextJs from '@/components/svgs/next-js'; 3 | import ReactJS from '@/components/svgs/react-js'; 4 | import { 5 | Building2, 6 | Code, 7 | GitPullRequest, 8 | LayoutDashboard, 9 | Trophy, 10 | } from 'lucide-react'; 11 | 12 | export const sideBarOptions = { 13 | general: [ 14 | { 15 | name: 'Dashboard', 16 | icon: LayoutDashboard, 17 | href: '/work/dashboard', 18 | isNew: false, 19 | }, 20 | // { 21 | // name: 'Leaderboard', 22 | // icon: Trophy, 23 | // href: '/work/leaderboard', 24 | // isNew: false, 25 | // }, 26 | { 27 | name: 'Organisations', 28 | icon: Building2, 29 | href: '/work/organisations', 30 | isNew: false, 31 | }, 32 | { 33 | name: 'My PRs', 34 | icon: GitPullRequest, 35 | href: '/work/my-pr', 36 | isNew: false, 37 | }, 38 | { 39 | name: 'Embed PRs', 40 | icon: Code, 41 | href: '/work/embed', 42 | isNew: false, 43 | }, 44 | ], 45 | }; 46 | 47 | export const frameWorksData = [ 48 | { 49 | name: 'HTML', 50 | icon: HTML, 51 | }, 52 | { 53 | name: 'ReactJs', 54 | icon: ReactJS, 55 | }, 56 | { 57 | name: 'NextJs', 58 | icon: NextJs, 59 | }, 60 | ]; 61 | -------------------------------------------------------------------------------- /src/lib/auth.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuthOptions, 3 | DefaultSession, 4 | Profile, 5 | SessionStrategy, 6 | } from 'next-auth'; 7 | import GithubProvider from 'next-auth/providers/github'; 8 | import { PrismaAdapter } from '@auth/prisma-adapter'; 9 | import prisma from './db'; 10 | import { Adapter } from 'next-auth/adapters'; 11 | import { JWT } from 'next-auth/jwt'; 12 | 13 | declare module 'next-auth' { 14 | interface Session extends DefaultSession { 15 | user: { 16 | image: string | undefined; 17 | id: string; 18 | name?: string | null; 19 | email?: string | null; 20 | username?: string | undefined; 21 | }; 22 | } 23 | } 24 | 25 | export const authOptions: AuthOptions = { 26 | adapter: PrismaAdapter(prisma) as Adapter, 27 | providers: [ 28 | GithubProvider({ 29 | clientId: process.env.GITHUB_CLIENT_ID || '', 30 | clientSecret: process.env.GITHUB_CLIENT_SECRET || '', 31 | }), 32 | ], 33 | pages: { 34 | signIn: '/auth', 35 | }, 36 | secret: process.env.NEXTAUTH_SECRET, 37 | session: { strategy: 'jwt' as SessionStrategy }, 38 | callbacks: { 39 | async jwt({ token, user, account, profile }: any) { 40 | if (account?.provider === 'github' && profile && 'login' in profile) { 41 | token.username = profile.login as string; 42 | } 43 | console.log('JWT callback - Token:', token); 44 | if (user) { 45 | token.admin = user.admin; 46 | } 47 | return token; 48 | }, 49 | async session({ 50 | session, 51 | token, 52 | }: { 53 | session: any; 54 | token: JWT; 55 | }): Promise { 56 | console.log('Session callback - Token:', token); 57 | if (token.sub) { 58 | try { 59 | const user = await prisma.user.update({ 60 | where: { id: token.sub }, 61 | data: { username: token.username as string }, 62 | }); 63 | console.log('Updated user:', user); 64 | 65 | session.user.id = user.id; 66 | session.user.admin = user.admin; 67 | session.user.username = user.username; 68 | session.user.name = user.name; 69 | } catch (error) { 70 | console.error('Error updating user:', error); 71 | } 72 | } 73 | console.log('Session callback - Final session:', session); 74 | return session; 75 | }, 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /src/lib/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | const prismaClientSingleton = () => { 4 | return new PrismaClient(); 5 | }; 6 | 7 | type PrismaClientSingleton = ReturnType; 8 | 9 | // eslint-disable-next-line 10 | const globalForPrisma = globalThis as unknown as { 11 | prisma: PrismaClientSingleton | undefined; 12 | }; 13 | 14 | const prisma = globalForPrisma.prisma ?? prismaClientSingleton(); 15 | 16 | export default prisma; 17 | 18 | if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; 19 | -------------------------------------------------------------------------------- /src/lib/profile.ts: -------------------------------------------------------------------------------- 1 | import prisma from './db'; 2 | 3 | export async function getUserProfile(username: string | undefined) { 4 | const user = await prisma.user.findUnique({ 5 | where: { username }, 6 | include: { 7 | pullRequests: { 8 | include: { 9 | org: true, 10 | }, 11 | }, 12 | contributedOrgs: true, 13 | }, 14 | }); 15 | 16 | return user; 17 | } 18 | 19 | export async function updatedUserProfile(e: any) { 20 | const updatedData = await prisma.user.update({ 21 | where: { 22 | // @ts-ignore 23 | id: user?.id, 24 | }, 25 | data: { 26 | isProfilePublic: e, 27 | }, 28 | }); 29 | } 30 | 31 | export async function getTotalViews(userId: string | undefined) { 32 | const totalViews = await prisma.profileView.aggregate({ 33 | where: { 34 | userId: userId, 35 | }, 36 | _sum: { 37 | viewCount: true, 38 | }, 39 | }); 40 | 41 | return totalViews._sum.viewCount || 0; 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import type { NextRequest } from 'next/server'; 3 | import { getToken } from 'next-auth/jwt'; 4 | 5 | export async function middleware(request: NextRequest) { 6 | console.log(process.env.NEXTAUTH_SECRET); 7 | const token = await getToken({ 8 | req: request, 9 | secret: process.env.NEXTAUTH_SECRET, 10 | }); 11 | console.log(token); 12 | 13 | if (request.nextUrl.pathname.startsWith('/work')) { 14 | if (!token) { 15 | return NextResponse.redirect(new URL('/', request.url)); 16 | } 17 | } 18 | 19 | return NextResponse.next(); 20 | } 21 | 22 | export const config = { 23 | matcher: ['/work/:path*'], 24 | }; 25 | -------------------------------------------------------------------------------- /src/store/sidebar.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | interface sideBarState { 4 | sidebarVisibility: boolean; 5 | toggleSidebarVisibility: (check: boolean) => void; 6 | } 7 | 8 | export const useSidebarStore = create()((set) => ({ 9 | sidebarVisibility: false, 10 | toggleSidebarVisibility: (check) => 11 | set((state) => ({ sidebarVisibility: !state.sidebarVisibility })), 12 | })); 13 | -------------------------------------------------------------------------------- /src/util/cn.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | export const capitalizeFirstLetter = (str: string) => { 2 | return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); 3 | }; 4 | -------------------------------------------------------------------------------- /src/util/types.ts: -------------------------------------------------------------------------------- 1 | export interface LeaderboardEntry { 2 | id: string; 3 | name: string; 4 | username: string; 5 | totalPoints: number; 6 | bounties: number; 7 | } 8 | 9 | export interface WebSocketMessage { 10 | type: string; 11 | data?: LeaderboardEntry[]; 12 | message?: string; 13 | } 14 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | const defaultTheme = require('tailwindcss/defaultTheme'); 3 | 4 | const svgToDataUri = require('mini-svg-data-uri'); 5 | 6 | const colors = require('tailwindcss/colors'); 7 | const { 8 | default: flattenColorPalette, 9 | } = require('tailwindcss/lib/util/flattenColorPalette'); 10 | /** @type {import('tailwindcss').Config} */ 11 | 12 | const config = { 13 | darkMode: ['class'], 14 | content: [ 15 | './pages/**/*.{ts,tsx}', 16 | './components/**/*.{ts,tsx}', 17 | './app/**/*.{ts,tsx}', 18 | './src/**/*.{ts,tsx}', 19 | ], 20 | prefix: '', 21 | theme: { 22 | container: { 23 | center: true, 24 | padding: '2rem', 25 | screens: { 26 | '2xl': '1400px', 27 | }, 28 | }, 29 | extend: { 30 | cursor: { 31 | custom: 'url(/custom-cursor.png), auto', 32 | }, 33 | fontFamily: { 34 | primary: ['var(--font-primary)', 'sans-serif'], 35 | secondary: ['var(--font-secondary)', 'sans-serif'], 36 | paragraph: ['var(--font-paragraph)', 'sans-serif'], 37 | }, 38 | colors: { 39 | border: 'hsl(var(--border))', 40 | input: 'hsl(var(--input))', 41 | ring: 'hsl(var(--ring))', 42 | background: 'hsl(var(--background))', 43 | foreground: 'hsl(var(--foreground))', 44 | primary: { 45 | DEFAULT: 'hsl(var(--primary))', 46 | foreground: 'hsl(var(--primary-foreground))', 47 | }, 48 | secondary: { 49 | DEFAULT: 'hsl(var(--secondary))', 50 | foreground: 'hsl(var(--secondary-foreground))', 51 | }, 52 | destructive: { 53 | DEFAULT: 'hsl(var(--destructive))', 54 | foreground: 'hsl(var(--destructive-foreground))', 55 | }, 56 | muted: { 57 | DEFAULT: 'hsl(var(--muted))', 58 | foreground: 'hsl(var(--muted-foreground))', 59 | }, 60 | accent: { 61 | DEFAULT: 'hsl(var(--accent))', 62 | foreground: 'hsl(var(--accent-foreground))', 63 | }, 64 | popover: { 65 | DEFAULT: 'hsl(var(--popover))', 66 | foreground: 'hsl(var(--popover-foreground))', 67 | }, 68 | card: { 69 | DEFAULT: 'hsl(var(--card))', 70 | foreground: 'hsl(var(--card-foreground))', 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: { height: '0' }, 81 | to: { height: 'var(--radix-accordion-content-height)' }, 82 | }, 83 | 'accordion-up': { 84 | from: { height: 'var(--radix-accordion-content-height)' }, 85 | to: { height: '0' }, 86 | }, 87 | spin: { 88 | from: { 89 | tranform: 'rotate(0deg)', 90 | }, 91 | to: { 92 | tranform: 'rotate(360deg)', 93 | }, 94 | }, 95 | spinRev: { 96 | from: { 97 | tranform: 'rotate(360deg)', 98 | }, 99 | to: { 100 | tranform: 'rotate(0deg)', 101 | }, 102 | }, 103 | shimmer: { 104 | from: { 105 | backgroundPosition: '0 0', 106 | }, 107 | to: { 108 | backgroundPosition: '-200% 0', 109 | }, 110 | }, 111 | }, 112 | animation: { 113 | 'accordion-down': 'accordion-down 0.2s ease-out', 114 | 'accordion-up': 'accordion-up 0.2s ease-out', 115 | shimmer: 'shimmer 2s linear infinite', 116 | spin: 'spin 1.5s linear infinite', 117 | spinRev: 'spinRev 2s linear infinite', 118 | }, 119 | }, 120 | }, 121 | plugins: [ 122 | require('tailwindcss-animate'), 123 | addVariablesForColors, 124 | function ({ matchUtilities, theme }: any) { 125 | matchUtilities( 126 | { 127 | 'bg-grid': (value: any) => ({ 128 | backgroundImage: `url("${svgToDataUri( 129 | ``, 130 | )}")`, 131 | }), 132 | 'bg-grid-small': (value: any) => ({ 133 | backgroundImage: `url("${svgToDataUri( 134 | ``, 135 | )}")`, 136 | }), 137 | 'bg-dot': (value: any) => ({ 138 | backgroundImage: `url("${svgToDataUri( 139 | ``, 140 | )}")`, 141 | }), 142 | }, 143 | { 144 | values: flattenColorPalette(theme('backgroundColor')), 145 | type: 'color', 146 | }, 147 | ); 148 | }, 149 | ], 150 | } satisfies Config; 151 | function addVariablesForColors({ addBase, theme }: any) { 152 | let allColors = flattenColorPalette(theme('colors')); 153 | let newVars = Object.fromEntries( 154 | Object.entries(allColors).map(([key, val]) => [`--${key}`, val]), 155 | ); 156 | 157 | addBase({ 158 | ':root': newVars, 159 | }); 160 | } 161 | export default config; 162 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------