From 8c1ab1435543145e4e05907fd9a25c926ab5c451 Mon Sep 17 00:00:00 2001 From: Aman Kumar Bairagi Date: Wed, 28 Aug 2024 12:07:28 +0530 Subject: [PATCH 1/3] add github link --- next.config.js | 10 + .../migration.sql | 20 ++ prisma/schema.prisma | 260 ++++++++++-------- public/platform/github.svg | 1 + public/platform/sol.svg | 28 ++ public/platform/upi.svg | 21 ++ src/app/api/github/callback/route.ts | 73 +++++ src/app/api/github/details/route.ts | 51 ++++ src/app/api/github/link/route.ts | 11 + src/app/payout-methods/page.tsx | 186 +++---------- src/components/GitHubLinkButton.tsx | 133 +++++++++ src/components/NewPayoutDialog.tsx | 58 ++-- src/components/PaymentMethodCard.tsx | 66 +++++ src/components/ViewAllDialog.tsx | 53 ++++ src/hooks/usePayoutMethod.ts | 55 ++++ 15 files changed, 734 insertions(+), 292 deletions(-) create mode 100644 prisma/migrations/20240828061415_add_github_link/migration.sql create mode 100644 public/platform/github.svg create mode 100644 public/platform/sol.svg create mode 100644 public/platform/upi.svg create mode 100644 src/app/api/github/callback/route.ts create mode 100644 src/app/api/github/details/route.ts create mode 100644 src/app/api/github/link/route.ts create mode 100644 src/components/GitHubLinkButton.tsx create mode 100644 src/components/PaymentMethodCard.tsx create mode 100644 src/components/ViewAllDialog.tsx create mode 100644 src/hooks/usePayoutMethod.ts diff --git a/next.config.js b/next.config.js index 0ee7325a2..ccf53f9c0 100644 --- a/next.config.js +++ b/next.config.js @@ -1,6 +1,16 @@ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'avatars.githubusercontent.com', + port: '', + pathname: '/**', + }, + ], + }, experimental: { serverActions: { allowedOrigins: ['localhost:3000', 'app.100xdevs.com', 'app2.100xdevs.com'] diff --git a/prisma/migrations/20240828061415_add_github_link/migration.sql b/prisma/migrations/20240828061415_add_github_link/migration.sql new file mode 100644 index 000000000..97f14b535 --- /dev/null +++ b/prisma/migrations/20240828061415_add_github_link/migration.sql @@ -0,0 +1,20 @@ +-- CreateTable +CREATE TABLE "GitHubLink" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "githubId" TEXT NOT NULL, + "username" TEXT NOT NULL, + "avatarUrl" TEXT, + "access_token" TEXT NOT NULL, + "profileUrl" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "GitHubLink_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "GitHubLink_userId_key" ON "GitHubLink"("userId"); + +-- AddForeignKey +ALTER TABLE "GitHubLink" ADD CONSTRAINT "GitHubLink_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a779d537d..2ee969079 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,7 +21,7 @@ model Course { content CourseContent[] purchasedBy UserPurchases[] certificate Certificate[] - certIssued Boolean @default(false) + certIssued Boolean @default(false) } model UserPurchases { @@ -65,12 +65,12 @@ model CourseContent { } model Certificate { - id String @id @default(cuid()) - slug String @default("certId") - user User @relation(fields: [userId], references: [id]) - userId String - course Course @relation(fields: [courseId], references: [id]) - courseId Int + id String @id @default(cuid()) + slug String @default("certId") + user User @relation(fields: [userId], references: [id]) + userId String + course Course @relation(fields: [courseId], references: [id]) + courseId Int @@unique([userId, courseId]) } @@ -85,40 +85,40 @@ model NotionMetadata { } model VideoMetadata { - id Int @id @default(autoincrement()) - contentId Int - video_1080p_mp4_1 String? // Link to 1080p mp4 quality video variant 1 - video_1080p_mp4_2 String? // Link to 1080p mp4 quality video variant 2 - video_1080p_mp4_3 String? // Link to 1080p mp4 quality video variant 3 - video_1080p_mp4_4 String? // Link to 1080p mp4 quality video variant 4 - video_1080p_1 String? // Link to 1080p quality video variant 1 - video_1080p_2 String? // Link to 1080p quality video variant 2 - video_1080p_3 String? // Link to 1080p quality video variant 3 - video_1080p_4 String? // Link to 1080p quality video variant 4 - video_720p_mp4_1 String? // Link to 720p mp4 quality video variant 1 - video_720p_mp4_2 String? // Link to 720p mp4 quality video variant 2 - video_720p_mp4_3 String? // Link to 720p mp4 quality video variant 3 - video_720p_mp4_4 String? // Link to 720p mp4 quality video variant 4 - video_720p_1 String? // Link to 720p quality video variant 1 - video_720p_2 String? // Link to 720p quality video variant 2 - video_720p_3 String? // Link to 720p quality video variant 3 - video_720p_4 String? // Link to 720p quality video variant 4 - video_360p_mp4_1 String? // Link to 360p mp4 quality video variant 1 - video_360p_mp4_2 String? // Link to 360p mp4 quality video variant 2 - video_360p_mp4_3 String? // Link to 360p mp4 quality video variant 3 - video_360p_mp4_4 String? // Link to 360p mp4 quality video variant 4 - video_360p_1 String? // Link to 360p quality video variant 1 - video_360p_2 String? // Link to 360p quality video variant 2 - video_360p_3 String? // Link to 360p quality video variant 3 - video_360p_4 String? // Link to 360p quality video variant 4 - subtitles String? // Link to subtitles file - segments Json? - content Content @relation(fields: [contentId], references: [id]) - slides String? // link to slides - thumbnail_mosiac_url String? - duration Int? - migration_status MigrationStatus @default(NOT_MIGRATED) - migration_pickup_time DateTime? + id Int @id @default(autoincrement()) + contentId Int + video_1080p_mp4_1 String? // Link to 1080p mp4 quality video variant 1 + video_1080p_mp4_2 String? // Link to 1080p mp4 quality video variant 2 + video_1080p_mp4_3 String? // Link to 1080p mp4 quality video variant 3 + video_1080p_mp4_4 String? // Link to 1080p mp4 quality video variant 4 + video_1080p_1 String? // Link to 1080p quality video variant 1 + video_1080p_2 String? // Link to 1080p quality video variant 2 + video_1080p_3 String? // Link to 1080p quality video variant 3 + video_1080p_4 String? // Link to 1080p quality video variant 4 + video_720p_mp4_1 String? // Link to 720p mp4 quality video variant 1 + video_720p_mp4_2 String? // Link to 720p mp4 quality video variant 2 + video_720p_mp4_3 String? // Link to 720p mp4 quality video variant 3 + video_720p_mp4_4 String? // Link to 720p mp4 quality video variant 4 + video_720p_1 String? // Link to 720p quality video variant 1 + video_720p_2 String? // Link to 720p quality video variant 2 + video_720p_3 String? // Link to 720p quality video variant 3 + video_720p_4 String? // Link to 720p quality video variant 4 + video_360p_mp4_1 String? // Link to 360p mp4 quality video variant 1 + video_360p_mp4_2 String? // Link to 360p mp4 quality video variant 2 + video_360p_mp4_3 String? // Link to 360p mp4 quality video variant 3 + video_360p_mp4_4 String? // Link to 360p mp4 quality video variant 4 + video_360p_1 String? // Link to 360p quality video variant 1 + video_360p_2 String? // Link to 360p quality video variant 2 + video_360p_3 String? // Link to 360p quality video variant 3 + video_360p_4 String? // Link to 360p quality video variant 4 + subtitles String? // Link to subtitles file + segments Json? + content Content @relation(fields: [contentId], references: [id]) + slides String? // link to slides + thumbnail_mosiac_url String? + duration Int? + migration_status MigrationStatus @default(NOT_MIGRATED) + migration_pickup_time DateTime? migrated_video_1080p_mp4_1 String? migrated_video_360p_mp4_1 String? migrated_video_720p_mp4_1 String? @@ -135,43 +135,57 @@ model Session { } model User { - id String @id @default(cuid()) - name String? - email String? @unique - token String? - sessions Session[] - purchases UserPurchases[] - videoProgress VideoProgress[] - comments Comment[] - votes Vote[] - discordConnect DiscordConnect? - disableDrm Boolean @default(false) - bunnyProxyEnabled Boolean @default(false) - bookmarks Bookmark[] - password String? - appxUserId String? - appxUsername String? - questions Question[] - answers Answer[] - certificate Certificate[] - upiIds UpiId[] @relation("UserUpiIds") - solanaAddresses SolanaAddress[] @relation("UserSolanaAddresses") + id String @id @default(cuid()) + name String? + email String? @unique + token String? + sessions Session[] + purchases UserPurchases[] + videoProgress VideoProgress[] + comments Comment[] + votes Vote[] + discordConnect DiscordConnect? + disableDrm Boolean @default(false) + bunnyProxyEnabled Boolean @default(false) + bookmarks Bookmark[] + password String? + appxUserId String? + appxUsername String? + questions Question[] + answers Answer[] + certificate Certificate[] + upiIds UpiId[] @relation("UserUpiIds") + solanaAddresses SolanaAddress[] @relation("UserSolanaAddresses") + githubUser GitHubLink? @relation("UserGithub") +} + +model GitHubLink { + id String @id @default(cuid()) + userId String @unique + user User @relation("UserGithub", fields: [userId], references: [id], onDelete: Cascade) + githubId String + username String + avatarUrl String? + access_token String + profileUrl String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model UpiId { - id Int @id @default(autoincrement()) - value String @db.VarChar(256) - userId String - user User @relation("UserUpiIds", fields: [userId], references: [id]) + id Int @id @default(autoincrement()) + value String @db.VarChar(256) + userId String + user User @relation("UserUpiIds", fields: [userId], references: [id]) @@unique([userId, value]) } model SolanaAddress { - id Int @id @default(autoincrement()) - value String @db.Char(44) - userId String - user User @relation("UserSolanaAddresses", fields: [userId], references: [id]) + id Int @id @default(autoincrement()) + value String @db.Char(44) + userId String + user User @relation("UserSolanaAddresses", fields: [userId], references: [id]) @@unique([userId, value]) } @@ -234,90 +248,92 @@ model Comment { votes Vote[] isPinned Boolean @default(false) } + model Question { - id Int @id @default(autoincrement()) - title String - content String - slug String @unique - createdAt DateTime @default(now()) - author User @relation(fields: [authorId], references: [id]) - authorId String - upvotes Int @default(0) - downvotes Int @default(0) + id Int @id @default(autoincrement()) + title String + content String + slug String @unique + createdAt DateTime @default(now()) + author User @relation(fields: [authorId], references: [id]) + authorId String + upvotes Int @default(0) + downvotes Int @default(0) totalanswers Int @default(0) - answers Answer[] - votes Vote[] - tags String[] - updatedAt DateTime @updatedAt + answers Answer[] + votes Vote[] + tags String[] + updatedAt DateTime @updatedAt @@index([authorId]) } model Answer { - id Int @id @default(autoincrement()) - content String - createdAt DateTime @default(now()) - question Question @relation(fields: [questionId], references: [id]) - questionId Int - author User @relation(fields: [authorId], references: [id]) - authorId String - votes Vote[] - upvotes Int @default(0) - downvotes Int @default(0) - totalanswers Int @default(0) - parentId Int? - responses Answer[] @relation("AnswerToAnswer") - parent Answer? @relation("AnswerToAnswer", fields: [parentId], references: [id]) - updatedAt DateTime @updatedAt + id Int @id @default(autoincrement()) + content String + createdAt DateTime @default(now()) + question Question @relation(fields: [questionId], references: [id]) + questionId Int + author User @relation(fields: [authorId], references: [id]) + authorId String + votes Vote[] + upvotes Int @default(0) + downvotes Int @default(0) + totalanswers Int @default(0) + parentId Int? + responses Answer[] @relation("AnswerToAnswer") + parent Answer? @relation("AnswerToAnswer", fields: [parentId], references: [id]) + updatedAt DateTime @updatedAt @@index([questionId]) @@index([authorId]) @@index([parentId]) } - model Vote { - id Int @id @default(autoincrement()) - questionId Int? - question Question? @relation(fields: [questionId], references: [id]) - answerId Int? - answer Answer? @relation(fields: [answerId], references: [id]) - commentId Int? - comment Comment? @relation(fields: [commentId], references: [id]) - userId String - user User @relation(fields: [userId], references: [id]) - voteType VoteType - createdAt DateTime @default(now()) + id Int @id @default(autoincrement()) + questionId Int? + question Question? @relation(fields: [questionId], references: [id]) + answerId Int? + answer Answer? @relation(fields: [answerId], references: [id]) + commentId Int? + comment Comment? @relation(fields: [commentId], references: [id]) + userId String + user User @relation(fields: [userId], references: [id]) + voteType VoteType + createdAt DateTime @default(now()) - @@unique([questionId, userId]) - @@unique([answerId, userId]) - @@unique([commentId, userId]) + @@unique([questionId, userId]) + @@unique([answerId, userId]) + @@unique([commentId, userId]) } model Event { - id Int @id @default(autoincrement()) - title String - start DateTime - end DateTime - videoLink String? - notes String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id Int @id @default(autoincrement()) + title String + start DateTime + end DateTime + videoLink String? + notes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } - enum VoteType { UPVOTE DOWNVOTE } + enum PostType { QUESTION ANSWER } + enum CommentType { INTRO DEFAULT } + enum MigrationStatus { NOT_MIGRATED IN_PROGRESS diff --git a/public/platform/github.svg b/public/platform/github.svg new file mode 100644 index 000000000..132ee4630 --- /dev/null +++ b/public/platform/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/platform/sol.svg b/public/platform/sol.svg new file mode 100644 index 000000000..bd349af26 --- /dev/null +++ b/public/platform/sol.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/platform/upi.svg b/public/platform/upi.svg new file mode 100644 index 000000000..7823cfb55 --- /dev/null +++ b/public/platform/upi.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/api/github/callback/route.ts b/src/app/api/github/callback/route.ts new file mode 100644 index 000000000..f1722cbb6 --- /dev/null +++ b/src/app/api/github/callback/route.ts @@ -0,0 +1,73 @@ +import prisma from "@/db"; +import { authOptions } from "@/lib/auth"; +import { getServerSession } from "next-auth"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(req: NextRequest) { + const code = req.nextUrl.searchParams.get('code'); + console.log('Received code:', code); + + const session = await getServerSession(authOptions); + console.log('Session:', session); + + if (!session?.user?.id || !code) { + return NextResponse.redirect(new URL('/payout-methods?error=invalid_session', req.url)); + } + + try { + // Exchange code for access token + const tokenResponse = await fetch('https://github.com/login/oauth/access_token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + client_id: process.env.GITHUB_ID, + client_secret: process.env.GITHUB_SECRET, + code, + }), + }); + + const tokenData = await tokenResponse.json(); + console.log('Token data:', tokenData); + + if (tokenData.error) { + throw new Error(tokenData.error_description); + } + + // Get user info from GitHub + const userResponse = await fetch('https://api.github.com/user', { + headers: { + Authorization: `token ${tokenData.access_token}`, + }, + }); + + const userData = await userResponse.json(); + console.log('User data:', userData); + + const existingLink = await prisma.gitHubLink.findUnique({ + where: { userId: session.user.id }, + }); + + if (existingLink) { + return NextResponse.redirect(new URL('/payout-methods?github_linked=true', req.url)); + } else { + await prisma.gitHubLink.create({ + data: { + userId: session.user.id, + githubId: userData.id.toString(), + username: userData.login, + access_token: tokenData.access_token, + avatarUrl: userData.avatar_url, + profileUrl: userData.html_url, + }, + }); + } + + return NextResponse.redirect(new URL('/payout-methods?github_linked=true', req.url)); + } catch (error) { + console.error('Error linking GitHub:', error); + return NextResponse.redirect(new URL('/payout-methods?error=github_link_failed', req.url)); + } +} \ No newline at end of file diff --git a/src/app/api/github/details/route.ts b/src/app/api/github/details/route.ts new file mode 100644 index 000000000..2c49d3b67 --- /dev/null +++ b/src/app/api/github/details/route.ts @@ -0,0 +1,51 @@ +import prisma from "@/db"; +import { authOptions } from "@/lib/auth"; +import { getServerSession } from "next-auth"; +import { NextRequest, NextResponse } from "next/server"; + + +export async function GET(req: NextRequest) { + const session = await getServerSession(authOptions); + + if (!session) { + return NextResponse.json({ message: 'Unauthorised', success: 'false' }, { status: 401 }); + } + + const githubData = await prisma.gitHubLink.findMany({ + where: { + userId: session?.user?.id + }, + select: { + avatarUrl: true, + username: true, + profileUrl: true, + } + }) + + if (!githubData) { + return NextResponse.json({ message: "Couldn't find any Linked github", success: 'false' }); + } + + return NextResponse.json({ message: "found data successsfully", data: githubData, success: 'true' }); +} + +export async function DELETE(req: NextRequest) { + const session = await getServerSession(authOptions); + + if (!session) { + return NextResponse.json({ message: 'Unauthorised', success: 'false' }, { status: 401 }); + } + + try { + + await prisma.gitHubLink.delete({ + where: { + userId: session?.user?.id + }, + }) + return NextResponse.json({ message: "Github unlinked succeessfully", success: 'true' }); + + } catch (error) { + return NextResponse.json({ message: "Something went wrong", error: error, success: 'false' }); + } +} \ No newline at end of file diff --git a/src/app/api/github/link/route.ts b/src/app/api/github/link/route.ts new file mode 100644 index 000000000..b3b36ee97 --- /dev/null +++ b/src/app/api/github/link/route.ts @@ -0,0 +1,11 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(req: NextRequest) { + const clientId = process.env.GITHUB_ID; + const redirectUri = `${process.env.NEXTAUTH_URL}/api/github/callback`; + const scope = 'read:user user:email'; + + const githubAuthUrl = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}`; + + return NextResponse.redirect(githubAuthUrl); +} \ No newline at end of file diff --git a/src/app/payout-methods/page.tsx b/src/app/payout-methods/page.tsx index a947d55be..b4bfb05c8 100644 --- a/src/app/payout-methods/page.tsx +++ b/src/app/payout-methods/page.tsx @@ -1,166 +1,70 @@ 'use client'; import { useEffect, useState } from 'react'; -import { Button } from '@/components/ui/button'; import NewPayoutDialog from '@/components/NewPayoutDialog'; -import { Trash } from 'lucide-react'; -import { - deleteSolanaAddress, - deleteUpiId, - getPayoutMethods, -} from '@/actions/payoutMethods'; -import { SolanaAddress, UpiId } from '@prisma/client'; -import { useAction } from '@/hooks/useAction'; -import { toast } from 'sonner'; +import { GitHubLinkButton } from '@/components/GitHubLinkButton'; +import SOL from "../../../public/platform/sol.svg" +import UPI from "../../../public/platform/upi.svg" +import { PayoutMethodCard } from '@/components/PaymentMethodCard'; +import { usePayoutMethods } from '@/hooks/usePayoutMethod'; export default function Page() { const [isDialogBoxOpen, setIsDialogBoxOpen] = useState(false); const [btnClicked, setBtnClicked] = useState(''); + const { upiAddresses, solanaAddresses, handleUpiDelete, handleSolanaDelete, fetchPayoutMethods } = usePayoutMethods(); - const openDialog = (e: any) => { + const openDialog = (method: string) => { setIsDialogBoxOpen(true); - setBtnClicked(e.target.id); - }; - - const closeDialog = () => setIsDialogBoxOpen(false); - - const [upiAddresses, setUpiAddresses] = useState([]); - const [solanaAddresses, setSolanaAddresses] = useState< - SolanaAddress[] | undefined - >([]); - - const fetchPayoutMethods = async () => { - const result = await getPayoutMethods(); - if (result) { - setUpiAddresses(result.upiIds); - setSolanaAddresses(result.solanaAddresses); - } - }; - - const { execute: executeDeleteUPI } = useAction(deleteUpiId, { - onSuccess: () => { - toast.success('UPI Address deleted successfully'); - }, - onError: () => { - toast.error('Failed to delete UPI id'); - }, - }); - - const { execute: executeDeleteSolana } = useAction(deleteSolanaAddress, { - onSuccess: () => { - toast.success('Solana Address deleted successfully'); - }, - onError: () => { - toast.error('Failed to delete Solana address'); - }, - }); - - const handleUpiDelete = (id: number) => { - executeDeleteUPI({ id }); - fetchPayoutMethods(); - }; - - const handleSolanaDelete = (id: number) => { - executeDeleteSolana({ id }); - fetchPayoutMethods(); + setBtnClicked(method); }; useEffect(() => { fetchPayoutMethods(); - }, []); + }, [isDialogBoxOpen]) - useEffect(() => { - fetchPayoutMethods(); - }, [isDialogBoxOpen]); + const closeDialog = () => setIsDialogBoxOpen(false); return (
-
-

Payout Methods

+
+

Payout Methods

-
-
-
-

UPI Addresses

- - -
-
- {upiAddresses?.length !== 0 ? ( - upiAddresses?.map((upi, index) => ( -
-

- {upi.value} -

- -
- )) - ) : ( -
-

No addresses added yet!

-
- )} -
-
+ +
+ +
-
-
-
-

Solana Addresses

- -
-
- {solanaAddresses?.length !== 0 ? ( - solanaAddresses?.map((sol, index) => ( -
-

- {sol.value} -

- -
- )) - ) : ( -
-

No addresses added yet!

-
- )} -
+ + + +
+

Apps

+
+
+
+
); diff --git a/src/components/GitHubLinkButton.tsx b/src/components/GitHubLinkButton.tsx new file mode 100644 index 000000000..79ae33253 --- /dev/null +++ b/src/components/GitHubLinkButton.tsx @@ -0,0 +1,133 @@ +'use client'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'sonner'; +import { Dialog, DialogTrigger, DialogContent } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar" +import Image from 'next/image'; +import GITHUB from "../../public/platform/github.svg"; +import Link from 'next/link'; +import { GitHubLink } from '@prisma/client'; + + +export const GitHubLinkButton = () => { + const [isProcessing, setIsProcessing] = useState(false); + const [githubData, setGithubData] = useState(null); + const searchParams = useSearchParams(); + const router = useRouter(); + + useEffect(() => { + if (searchParams.get('github_linked') === 'true') { + router.push('/payout-methods') + toast.success('Github account linked successfully') + } else if (searchParams.get('error') === 'github_link_failed') { + toast.error("Couldn't link with your github , Try again after sometime") + } + }, [searchParams]); + + const getGithubData = async () => { + const response = await fetch('/api/github/details'); + const resp = await response.json() + setGithubData(resp.data[0]); + } + + + const handleUnlinkAccount = async () => { + setIsProcessing(true); + const response = await fetch('/api/github/details', { + method: 'DELETE' + }); + setIsProcessing(false); + const resp = await response.json() + console.log(resp); + if (resp.success) { + toast.success(resp.message); + getGithubData() + } + else { + toast.error(resp.message); + } + } + + + + useEffect(() => { + // fetch the saved github linked data + getGithubData() + }, [searchParams]) + + const handleLinkGitHub = async () => { + setIsProcessing(true); + window.location.href = '/api/github/link'; + }; + + return ( + <> +
+
+
+

Github

+

Link your Github account

+
+
+ +
+ {githubData ? ( +
+ {/* {githubData?.avatarUrl} */} + + + + + + +
+ + + CN + +
+

@{githubData?.username}

+ {githubData?.profileUrl} + {/*

@oliviadavis

*/} +
+
+ +
+
+ +
+ ) : ( + + )} +
+ +
+ Landscape +
+
+ + ); +}; diff --git a/src/components/NewPayoutDialog.tsx b/src/components/NewPayoutDialog.tsx index 236f00e43..2cde1edde 100644 --- a/src/components/NewPayoutDialog.tsx +++ b/src/components/NewPayoutDialog.tsx @@ -1,5 +1,4 @@ 'use client'; -import { X } from 'lucide-react'; import { Input } from './ui/input'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -11,6 +10,13 @@ import { toast } from 'sonner'; import { useAction } from '@/hooks/useAction'; import { useState } from 'react'; import { Loader } from './Loader'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; interface DialogProps { isOpen: boolean; @@ -81,46 +87,40 @@ export default function NewPayoutDialog({ throw new Error('Invalid method'); } } catch (error) { + setIsLoading(false); console.error(error); toast.error('An unexpected error occured'); } }; return ( -
-
-

- Add {title} address -

- -
-
-
+ <> + + + + Add {title} Address + + +
{errors[fieldName] && ( -

- {errors[fieldName]?.message} -

- )} +

+ {errors[fieldName]?.message} +

+ )} +
- + -
-
-
+ + + + ); } diff --git a/src/components/PaymentMethodCard.tsx b/src/components/PaymentMethodCard.tsx new file mode 100644 index 000000000..781835a1d --- /dev/null +++ b/src/components/PaymentMethodCard.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import Image from 'next/image'; +import { Button } from '@/components/ui/button'; +import { Dialog, DialogTrigger } from "@/components/ui/dialog"; +import { ViewAllDialog } from './ViewAllDialog'; + +interface PayoutMethodCardProps { + title: string; + description: string; + imageSrc: string; + addresses: | Array<{ + id: number; + value: string; + userId: string; + }> + | undefined; + onAdd: (method: string) => void; + id: string; + onDelete: (id: number) => void; +} + +export const PayoutMethodCard: React.FC = ({ + title, + description, + imageSrc, + addresses, + onAdd, + id, + onDelete +}) => { + return ( +
+
+

{title}

+

{description}

+
+ +
+ + + {addresses?.length !== 0 && ( + + + + + + + )} +
+ +
+ {title} +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/ViewAllDialog.tsx b/src/components/ViewAllDialog.tsx new file mode 100644 index 000000000..2c1463fb0 --- /dev/null +++ b/src/components/ViewAllDialog.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { Button } from '@/components/ui/button'; +import { DialogContent, DialogHeader } from "@/components/ui/dialog"; +import { TrashIcon } from 'lucide-react'; +import { toast } from 'sonner'; + +interface ViewAllDialogProps { + title: string; + addresses: | Array<{ + id: number; + value: string; + userId: string; + }> + | undefined; + onDelete: (id: number) => void; +} + +export const ViewAllDialog: React.FC = ({ title, addresses, onDelete }) => { + return ( + + +
+
+

{title}

+ Click on address to copy +
+
+
+
+ {addresses?.length !== 0 ? ( + addresses?.map((address, index) => ( +
+
{ + navigator.clipboard.writeText(address.value); + toast.success('Copied to clipboard') + }}> +

{address.value.slice(0, 20)}...

+
+ +
+ )) + ) : ( +
+

No addresses added yet!

+
+ )} +
+
+ ); +}; \ No newline at end of file diff --git a/src/hooks/usePayoutMethod.ts b/src/hooks/usePayoutMethod.ts new file mode 100644 index 000000000..fde3aefcc --- /dev/null +++ b/src/hooks/usePayoutMethod.ts @@ -0,0 +1,55 @@ +"use client"; +import { useState, useEffect } from 'react'; +import { useAction } from '@/hooks/useAction'; +import { toast } from 'sonner'; +import { + deleteSolanaAddress, + deleteUpiId, + getPayoutMethods, +} from '@/actions/payoutMethods'; +import { SolanaAddress, UpiId } from '@prisma/client'; + +export const usePayoutMethods = () => { + const [upiAddresses, setUpiAddresses] = useState([]); + const [solanaAddresses, setSolanaAddresses] = useState([]); + + const fetchPayoutMethods = async () => { + const result = await getPayoutMethods(); + if (result) { + setUpiAddresses(result.upiIds); + setSolanaAddresses(result.solanaAddresses); + } + }; + + const { execute: executeDeleteUPI } = useAction(deleteUpiId, { + onSuccess: () => { + toast.success('UPI Address deleted successfully'); + fetchPayoutMethods(); + }, + onError: () => { + toast.error('Failed to delete UPI id'); + }, + }); + + const { execute: executeDeleteSolana } = useAction(deleteSolanaAddress, { + onSuccess: () => { + toast.success('Solana Address deleted successfully'); + fetchPayoutMethods(); + }, + onError: () => { + toast.error('Failed to delete Solana address'); + }, + }); + + useEffect(() => { + fetchPayoutMethods(); + }, []); + + return { + upiAddresses, + solanaAddresses, + fetchPayoutMethods, + handleUpiDelete: (id: number) => executeDeleteUPI({ id }), + handleSolanaDelete: (id: number) => executeDeleteSolana({ id }), + }; +}; \ No newline at end of file From 8f0b8b5c26dd0413221140d05955f57fffc104c3 Mon Sep 17 00:00:00 2001 From: Aman Kumar Bairagi Date: Wed, 28 Aug 2024 12:24:40 +0530 Subject: [PATCH 2/3] add github link --- src/app/api/github/callback/route.ts | 8 +++----- src/app/api/github/details/route.ts | 13 +++++-------- src/app/api/github/link/route.ts | 4 ++-- src/components/PaymentMethodCard.tsx | 2 +- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/app/api/github/callback/route.ts b/src/app/api/github/callback/route.ts index f1722cbb6..e75c1dc1b 100644 --- a/src/app/api/github/callback/route.ts +++ b/src/app/api/github/callback/route.ts @@ -5,10 +5,8 @@ import { NextRequest, NextResponse } from "next/server"; export async function GET(req: NextRequest) { const code = req.nextUrl.searchParams.get('code'); - console.log('Received code:', code); const session = await getServerSession(authOptions); - console.log('Session:', session); if (!session?.user?.id || !code) { return NextResponse.redirect(new URL('/payout-methods?error=invalid_session', req.url)); @@ -50,9 +48,7 @@ export async function GET(req: NextRequest) { where: { userId: session.user.id }, }); - if (existingLink) { - return NextResponse.redirect(new URL('/payout-methods?github_linked=true', req.url)); - } else { + if (!existingLink) { await prisma.gitHubLink.create({ data: { userId: session.user.id, @@ -63,6 +59,8 @@ export async function GET(req: NextRequest) { profileUrl: userData.html_url, }, }); + } else { + return NextResponse.redirect(new URL('/payout-methods?github_linked=true', req.url)); } return NextResponse.redirect(new URL('/payout-methods?github_linked=true', req.url)); diff --git a/src/app/api/github/details/route.ts b/src/app/api/github/details/route.ts index 2c49d3b67..80b89e1e3 100644 --- a/src/app/api/github/details/route.ts +++ b/src/app/api/github/details/route.ts @@ -1,10 +1,9 @@ import prisma from "@/db"; import { authOptions } from "@/lib/auth"; import { getServerSession } from "next-auth"; -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; - -export async function GET(req: NextRequest) { +export async function GET() { const session = await getServerSession(authOptions); if (!session) { @@ -20,7 +19,7 @@ export async function GET(req: NextRequest) { username: true, profileUrl: true, } - }) + }); if (!githubData) { return NextResponse.json({ message: "Couldn't find any Linked github", success: 'false' }); @@ -29,7 +28,7 @@ export async function GET(req: NextRequest) { return NextResponse.json({ message: "found data successsfully", data: githubData, success: 'true' }); } -export async function DELETE(req: NextRequest) { +export async function DELETE() { const session = await getServerSession(authOptions); if (!session) { @@ -37,14 +36,12 @@ export async function DELETE(req: NextRequest) { } try { - await prisma.gitHubLink.delete({ where: { userId: session?.user?.id }, - }) + }); return NextResponse.json({ message: "Github unlinked succeessfully", success: 'true' }); - } catch (error) { return NextResponse.json({ message: "Something went wrong", error: error, success: 'false' }); } diff --git a/src/app/api/github/link/route.ts b/src/app/api/github/link/route.ts index b3b36ee97..e634dd2f9 100644 --- a/src/app/api/github/link/route.ts +++ b/src/app/api/github/link/route.ts @@ -1,6 +1,6 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; -export async function GET(req: NextRequest) { +export async function GET() { const clientId = process.env.GITHUB_ID; const redirectUri = `${process.env.NEXTAUTH_URL}/api/github/callback`; const scope = 'read:user user:email'; diff --git a/src/components/PaymentMethodCard.tsx b/src/components/PaymentMethodCard.tsx index 781835a1d..01f30a438 100644 --- a/src/components/PaymentMethodCard.tsx +++ b/src/components/PaymentMethodCard.tsx @@ -47,7 +47,7 @@ export const PayoutMethodCard: React.FC = ({ View All - + )}
From 60f67d792500f0c6093c7624939d706151c30be9 Mon Sep 17 00:00:00 2001 From: Aman Kumar Bairagi Date: Wed, 28 Aug 2024 12:26:22 +0530 Subject: [PATCH 3/3] update .env.example with necessary keys --- .env.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.env.example b/.env.example index 4cb277756..f6dff95ae 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,8 @@ CACHE_EXPIRE_S = 10 ADMINS = "Random,example@gmail.com" NEXT_PUBLIC_DISABLE_FEATURES = "featurea,featureb,featurec" REDIS_URL= +GITHUB_ID= +GITHUB_SECRET= COHORT3_DISCORD_ACCESS_KEY =