├── .env.example ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── README.md ├── app ├── api │ ├── ai-review │ │ └── route.ts │ ├── auth │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── discord-announcement │ │ └── route.ts │ ├── memory │ │ └── route.ts │ ├── message │ │ └── route.ts │ ├── profile │ │ ├── query │ │ │ └── route.ts │ │ └── sign │ │ │ └── route.ts │ ├── self-check │ │ └── route.ts │ ├── social │ │ ├── discord │ │ │ ├── check-guild │ │ │ │ └── route.ts │ │ │ └── verify │ │ │ │ └── route.ts │ │ ├── github │ │ │ └── verify │ │ │ │ └── route.ts │ │ └── twitter │ │ │ ├── check-actions │ │ │ └── route.ts │ │ │ └── verify │ │ │ └── route.ts │ └── upload │ │ └── route.ts ├── board │ └── [id] │ │ └── page.tsx ├── boards │ ├── create │ │ └── page.tsx │ ├── joined │ │ └── page.tsx │ └── page.tsx ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── assets ├── BountyBoard.png ├── Screenshot.png ├── ScreenshotHome.png └── ScreenshotTask.png ├── components.json ├── components ├── BoardActionsDropdown.tsx ├── BoardCard.tsx ├── BoardForm.tsx ├── Boards.tsx ├── BoardsPageSkeleton.tsx ├── ConnectWalletButton.tsx ├── CreateTaskModal.tsx ├── DynamicModal.tsx ├── ImageUpload.tsx ├── MemberSubmissionTable.tsx ├── Navigation.tsx ├── ProfileSettingsModal.tsx ├── SubmissionDetailsModal.tsx ├── SubmitProofModal.tsx ├── TaskList.tsx └── ui │ ├── Address.tsx │ ├── alert.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── card.tsx │ ├── checkbox.tsx │ ├── command.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── label.tsx │ ├── loading.tsx │ ├── popover.tsx │ ├── select.tsx │ ├── skeleton.tsx │ ├── table.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── tooltip.tsx │ └── use-toast.ts ├── constants ├── BountyBoard.json ├── UserProfile.json ├── attestaion.ts └── contract-address.ts ├── contract ├── EVM │ ├── .github │ │ └── workflows │ │ │ └── test.yml │ ├── .gitignore │ ├── foundry.toml │ ├── script │ │ ├── BountyBoard.s.sol │ │ ├── DeployMockERC20.s.sol │ │ ├── MintToken.s.sol │ │ └── UserProfile.s.sol │ ├── src │ │ ├── BoardStorage.sol │ │ ├── BoardView.sol │ │ ├── BountyBoard.sol │ │ ├── BountyBoardold.sol │ │ ├── IBountyBoard.sol │ │ ├── MockERC20.sol │ │ ├── SubmissionManager.sol │ │ ├── TaskManager.sol │ │ └── UserProfile.sol │ └── test │ │ ├── BountyBoard.t.sol │ │ └── UserProfile.t.sol └── SVM │ ├── Cargo.toml │ └── src │ ├── error.rs │ ├── instruction.rs │ ├── lib.rs │ ├── processor.rs │ └── state.rs ├── eliza-add ├── agent │ └── src │ │ └── index.ts ├── characters │ └── BountyBoard.character.json └── packages │ ├── client-direct │ └── src │ │ └── index.ts │ ├── client-discord │ └── src │ │ ├── actions │ │ ├── announcement.ts │ │ ├── chat_with_attachments.ts │ │ ├── download_media.ts │ │ ├── joinvoice.ts │ │ ├── leavevoice.ts │ │ ├── summarize_conversation.ts │ │ └── transcribe_media.ts │ │ ├── attachments.ts │ │ ├── enviroment.ts │ │ ├── index.ts │ │ ├── messages.ts │ │ ├── providers │ │ ├── channelState.ts │ │ └── voiceState.ts │ │ ├── templates.ts │ │ ├── utils.ts │ │ └── voice.ts │ └── plugin-bountyboard-evm │ ├── package.json │ ├── src │ ├── actions │ │ ├── BountyBoard.json │ │ ├── query.ts │ │ └── review.ts │ ├── index.ts │ ├── providers │ │ └── wallet.ts │ ├── templates │ │ └── index.ts │ └── types │ │ └── index.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── graphql └── queries.ts ├── hooks ├── useAddressProfiles.ts ├── useContract.ts └── useMediaQuery.ts ├── lib ├── utils.ts └── uuid.ts ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── providers ├── TelegramAuthContext.tsx ├── Web3Providers.tsx ├── config.ts ├── monad.ts └── my-anvil.ts ├── public ├── home │ ├── CommunityBuilding.jpg │ ├── DeveloperEducation.jpg │ └── ProductTesting.jpg ├── index-head.png ├── logo.png ├── logo.svg ├── next.svg └── vercel.svg ├── scripts └── generate-aes-key.ts ├── services └── aiReview.ts ├── store └── userStore.ts ├── tailwind.config.ts ├── tsconfig.json ├── types ├── profile.ts └── types.ts └── utils ├── chain.ts ├── encryption-server.ts └── encryption.ts /.env.example: -------------------------------------------------------------------------------- 1 | TWITTER_CLIENT_ID= 2 | TWITTER_CLIENT_SECRET= 3 | TWITTER_BEARER_TOKEN= 4 | DISCORD_CLIENT_ID= 5 | DISCORD_CLIENT_SECRET= 6 | DISCORD_BOT_TOKEN= 7 | GITHUB_CLIENT_ID= 8 | GITHUB_CLIENT_SECRET= 9 | NEXTAUTH_URL=https://localhost:3000 10 | NEXT_PUBLIC_API_URL=https://localhost:3000 11 | 12 | ELIZA_API_URL=http://localhost:3030 13 | ELIZA_AGENT_ID= 14 | ELIZA_AGENT_USER_ID= 15 | DISCORD_AGENT_BOT= 16 | SMMS_TOKEN= 17 | 18 | SIGNER_ADDRESS_PRIVATE_KEY= 19 | 20 | # Particle Network Configuration 21 | NEXT_PUBLIC_PARTICLE_PROJECT_ID= 22 | NEXT_PUBLIC_PARTICLE_CLIENT_KEY= 23 | NEXT_PUBLIC_PARTICLE_APP_ID= 24 | 25 | # WalletConnect 26 | NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= 27 | 28 | # Social Auth 29 | TWITTER_CLIENT_ID= 30 | TWITTER_CLIENT_SECRET= 31 | DISCORD_CLIENT_ID= 32 | DISCORD_CLIENT_SECRET= 33 | GITHUB_CLIENT_ID= 34 | GITHUB_CLIENT_SECRET= 35 | 36 | # Other configurations 37 | NEXT_PUBLIC_GATEWAY_URL= 38 | NEXT_PUBLIC_IPFS_GATEWAY= -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | out -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals"], 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "rules": { 7 | "@typescript-eslint/no-explicit-any": "warn", 8 | "@typescript-eslint/no-unused-vars": "warn", 9 | "react-hooks/rules-of-hooks": "error", 10 | "react-hooks/exhaustive-deps": "warn" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | run-*.json 40 | 41 | certificates 42 | 43 | /draft -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "contract/EVM/lib/forge-std"] 2 | path = contract/EVM/lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "contract/EVM/lib/openzeppelin-contracts"] 5 | path = contract/EVM/lib/openzeppelin-contracts 6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.localesPaths": [ 3 | "contract/lib/linea-attestation-registry/explorer/src/assets/locales", 4 | "contract/lib/linea-attestation-registry/snap/packages/snap/locales" 5 | ] 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BountyBoard 2 | 3 | ![Bounty Board](./assets/BountyBoard.png) 4 | 5 | BountyBoard is a decentralized Web3 community engagement platform built on blockchain technology, aiming to automate task management and reward distribution, thereby increasing community participation and efficiency. 6 | 7 | ## Key Advantages: 8 | 9 | * **Decentralization**: Utilizes blockchain technology to ensure transparency and trust in platform operations. 10 | * **Smart Contracts**: Automates task management and reward distribution using smart contracts, improving efficiency and transparency. 11 | * **AI-Agent (Eliza)**: Provides intelligent Q&A, task auditing, data analysis, and task notifications to enhance community interaction and efficiency. 12 | * **User-Friendly Interface**: Offers a simple and intuitive interface for users to create and manage tasks, track progress, and monitor participation. 13 | 14 | ## Key Features 15 | 16 | **Task Management**: 17 | 18 | * **Multiple Task Types**: Supports various task types to cater to different community activities and engagement needs. 19 | * **Customizable Completion Criteria**: Allows users to set specific conditions for task completion, ensuring tasks are tailored to individual project requirements. 20 | * **Progress Tracking Dashboard**: Provides real-time monitoring of task progress and participant performance. 21 | 22 | **Reward Distribution**: 23 | 24 | * **Smart Contract Automation**: Automates reward distribution using smart contracts, ensuring transparency and fairness. 25 | * **Multiple Token Support**: Supports various tokens for rewards, providing flexibility to cater to different community preferences. 26 | 27 | **Community Management**: 28 | 29 | * **AI-Agent (Eliza)**: Offers intelligent Q&A, task auditing, data analysis, and task notifications to enhance community interaction and efficiency. 30 | * **Community Verification**: Enables community validation of AI-Agent operations, ensuring privacy and security. 31 | 32 | ## Start 33 | 34 | ### Install 35 | 36 | First, install the dependencies: 37 | 38 | ```bash 39 | pnpm install 40 | ``` 41 | 42 | Second, set the environment variables: 43 | 44 | ```bash 45 | cp .env.example .env 46 | ``` 47 | 48 | ### Run 49 | 50 | ```bash 51 | pnpm dev 52 | ``` 53 | 54 | ## Set up the agent 55 | 56 | First, clone the agent repository: 57 | 58 | ```bash 59 | git clone https://github.com/ai16z/eliza.git 60 | ``` 61 | 62 | Second, set the environment variables, you need to set the `DISCORD_APPLICATION_ID`, `DISCORD_API_TOKEN`,`BOUNTYBOARD_PRIVATE_KEY`(the private key of the account that will review the tasks), `BOUNTYBOARD_ADDRESS`, `GAIANET_MODEL`, `GAIANET_SERVER_URL`, `GAIANET_EMBEDDING_MODEL`, `USE_GAIANET_EMBEDDING`, `DSTACK_SIMULATOR_ENDPOINT`(the endpoint of the TEE simulator), `WALLET_SECRET_SALT`(the TEE secret salt) in the `.env` file. 63 | 64 | ```bash 65 | cp .env.example .env 66 | ``` 67 | 68 | Third, install the dependencies: 69 | 70 | ```bash 71 | pnpm install 72 | ``` 73 | 74 | Fourth, copy the `eliza-add` directory to the `eliza` directory: 75 | 76 | Then, you can build and start the agent: 77 | 78 | ```bash 79 | pnpm build 80 | pnpm start --characters="characters/BountyBoard.character.json" 81 | ``` 82 | 83 | ## Screenshot 84 | 85 | **Home** 86 | 87 | ![Bounty Board](./assets/ScreenshotHome.png) 88 | 89 | **Task** 90 | 91 | ![Bounty Board](./assets/ScreenshotTask.png) 92 | 93 | -------------------------------------------------------------------------------- /app/api/ai-review/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { AIReviewService } from "@/services/aiReview"; 3 | 4 | const aiReviewService = new AIReviewService(); 5 | 6 | export async function POST(req: NextRequest) { 7 | try { 8 | const requestData = await req.json(); 9 | const result = await aiReviewService.review(requestData); 10 | return NextResponse.json(result); 11 | } catch (error) { 12 | console.error("AI Review API error:", error); 13 | return NextResponse.json( 14 | { error: error instanceof Error ? error.message : "AI Review failed" }, 15 | { status: 500 } 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { NextAuthOptions } from "next-auth"; 2 | import TwitterProvider from "next-auth/providers/twitter"; 3 | import DiscordProvider from "next-auth/providers/discord"; 4 | import GithubProvider from "next-auth/providers/github"; 5 | 6 | export const authOptions: NextAuthOptions = { 7 | providers: [ 8 | TwitterProvider({ 9 | clientId: process.env.TWITTER_CLIENT_ID!, 10 | clientSecret: process.env.TWITTER_CLIENT_SECRET!, 11 | version: "2.0", 12 | authorization: { 13 | params: { 14 | scope: [ 15 | "tweet.read", 16 | "users.read", 17 | "follows.read", 18 | "like.read" 19 | ].join(" ") 20 | } 21 | }, 22 | httpOptions: { 23 | timeout: 1000000, 24 | }, 25 | }), 26 | DiscordProvider({ 27 | clientId: process.env.DISCORD_CLIENT_ID!, 28 | clientSecret: process.env.DISCORD_CLIENT_SECRET!, 29 | authorization: { 30 | params: { 31 | scope: 'identify email guilds', 32 | }, 33 | }, 34 | }), 35 | GithubProvider({ 36 | clientId: process.env.GITHUB_CLIENT_ID!, 37 | clientSecret: process.env.GITHUB_CLIENT_SECRET!, 38 | authorization: { 39 | params: { 40 | scope: 'read:user user:email', 41 | }, 42 | }, 43 | }), 44 | ], 45 | callbacks: { 46 | async jwt({ token, account }: { token: any; account: any; }) { 47 | if (account) { 48 | token.id = account.providerAccountId; 49 | token.accessToken = account.access_token; 50 | } 51 | return token; 52 | }, 53 | async session({ session, token }: { session: any; token: any }) { 54 | if (session.user) { 55 | session.user.id = token.id as string; 56 | (session as any).accessToken = token.accessToken; 57 | } 58 | return session; 59 | }, 60 | async redirect({ url, baseUrl }) { 61 | return url.startsWith(baseUrl) 62 | ? url.replace('http://', 'https://') 63 | : baseUrl.replace('http://', 'https://'); 64 | }, 65 | }, 66 | debug: process.env.NODE_ENV === 'development', 67 | }; 68 | 69 | const handler = NextAuth(authOptions); 70 | export { handler as GET, handler as POST }; -------------------------------------------------------------------------------- /app/api/discord-announcement/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { stringToUuid } from "@/lib/uuid"; 3 | 4 | const elizaAgentUserId = process.env.ELIZA_AGENT_USER_ID || ""; 5 | const elizaAgentId = process.env.ELIZA_AGENT_ID || ""; 6 | const elizaAgentUrl = `${process.env.ELIZA_API_URL}/${elizaAgentId}/message`; 7 | const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN; 8 | const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY; 9 | const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent'; 10 | const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN; 11 | const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID; 12 | 13 | export async function POST(req: NextRequest) { 14 | try { 15 | const { channelId, type, data } = await req.json(); 16 | 17 | if (!channelId || !type || !data) { 18 | return NextResponse.json( 19 | { error: "Missing required fields" }, 20 | { status: 400 } 21 | ); 22 | } 23 | 24 | let announcementText = ''; 25 | 26 | if (elizaAgentUrl && elizaAgentId) { 27 | const aiResponse = await fetch(elizaAgentUrl, { 28 | method: "POST", 29 | headers: { 30 | "Content-Type": "application/json", 31 | }, 32 | body: JSON.stringify({ 33 | roomId: stringToUuid(channelId + "-" + elizaAgentId), 34 | userId: stringToUuid(elizaAgentUserId), 35 | userName: "user", 36 | content: { 37 | text: `Please format the following information into a clear and concise announcement. Use appropriate emojis and maintain a professional tone. Focus only on the essential details: 38 | Type: ${type} 39 | Content: ${JSON.stringify(data)}`, 40 | attachments: [], 41 | source: "direct", 42 | }, 43 | }), 44 | }); 45 | 46 | if (!aiResponse.ok) { 47 | const errorDetails = await aiResponse.json(); 48 | throw new Error(errorDetails.error || "Failed to get AI response"); 49 | } 50 | 51 | const reader = aiResponse.body?.getReader(); 52 | let aiContent = ""; 53 | 54 | if (reader) { 55 | while (true) { 56 | const { done, value } = await reader.read(); 57 | if (done) break; 58 | const chunk = new TextDecoder().decode(value); 59 | aiContent += chunk; 60 | } 61 | } 62 | 63 | let responses; 64 | try { 65 | const parsedContent = JSON.parse(aiContent); 66 | if (Array.isArray(parsedContent)) { 67 | responses = parsedContent; 68 | } else if (parsedContent.responses) { 69 | responses = parsedContent.responses; 70 | } else { 71 | responses = [parsedContent]; 72 | } 73 | announcementText = responses[0].content?.text || responses[0].text; 74 | } catch (error) { 75 | console.error("Eliza Parse Error:", error); 76 | throw new Error("Failed to parse Eliza response"); 77 | } 78 | } 79 | else if (GOOGLE_API_KEY) { 80 | const prompt = { 81 | announcement_request: { 82 | type, 83 | content: data 84 | }, 85 | instructions: "Format the given information into a clear and concise announcement. Use appropriate emojis and maintain a professional tone. Focus on essential details. Return only the formatted announcement text without any JSON structure or additional formatting.", 86 | example_response: "🎉 New Update Available!\nWe're excited to announce the latest features...", 87 | }; 88 | 89 | const geminiResponse = await fetch(`${GEMINI_API_URL}?key=${GOOGLE_API_KEY}`, { 90 | method: 'POST', 91 | headers: { 92 | 'Content-Type': 'application/json', 93 | }, 94 | body: JSON.stringify({ 95 | contents: [{ 96 | parts: [{ 97 | text: JSON.stringify(prompt) 98 | }] 99 | }] 100 | }) 101 | }); 102 | 103 | if (!geminiResponse.ok) { 104 | throw new Error('Failed to get Gemini API response'); 105 | } 106 | 107 | const geminiData = await geminiResponse.json(); 108 | announcementText = geminiData.candidates[0].content.parts[0].text.trim(); 109 | } else { 110 | throw new Error("No AI service configured"); 111 | } 112 | 113 | if (!announcementText) { 114 | throw new Error("Could not generate announcement text"); 115 | } 116 | 117 | if (DISCORD_BOT_TOKEN) { 118 | const discordResponse = await fetch( 119 | `https://discord.com/api/v10/channels/${channelId}/messages`, 120 | { 121 | method: "POST", 122 | headers: { 123 | Authorization: `Bot ${DISCORD_BOT_TOKEN}`, 124 | "Content-Type": "application/json", 125 | }, 126 | body: JSON.stringify({ 127 | content: announcementText, 128 | }), 129 | } 130 | ); 131 | 132 | if (!discordResponse.ok) { 133 | const errorData = await discordResponse.json(); 134 | throw new Error(errorData.message || "Failed to send Discord message"); 135 | } 136 | } 137 | 138 | if (TELEGRAM_BOT_TOKEN && TELEGRAM_CHAT_ID) { 139 | const telegramResponse = await fetch( 140 | `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, 141 | { 142 | method: "POST", 143 | headers: { 144 | "Content-Type": "application/json", 145 | }, 146 | body: JSON.stringify({ 147 | chat_id: TELEGRAM_CHAT_ID, 148 | text: announcementText, 149 | parse_mode: "HTML", 150 | }), 151 | } 152 | ); 153 | 154 | if (!telegramResponse.ok) { 155 | const errorData = await telegramResponse.json(); 156 | throw new Error(errorData.description || "Failed to send Telegram message"); 157 | } 158 | } 159 | 160 | return NextResponse.json({ 161 | success: true, 162 | message: "Announcement sent successfully", 163 | }); 164 | } catch (error) { 165 | console.error("Error in announcement:", error); 166 | return NextResponse.json( 167 | { 168 | error: "Failed to process announcement", 169 | details: error instanceof Error ? error.message : error, 170 | }, 171 | { status: 500 } 172 | ); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /app/api/memory/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { v4 as uuidv4 } from "uuid"; 3 | import { stringToUuid } from "@/lib/uuid"; 4 | 5 | const elizaAgentUserId = process.env.ELIZA_AGENT_USER_ID; 6 | 7 | const elizaAgentId = process.env.ELIZA_AGENT_ID; 8 | 9 | const elizaAgentUrl = `${process.env.ELIZA_API_URL}/${elizaAgentId}/memories`; 10 | 11 | export async function POST(req: NextRequest) { 12 | try { 13 | const { content, channelId } = await req.json(); 14 | 15 | if (!content || !channelId) { 16 | return NextResponse.json( 17 | { error: "Missing required fields" }, 18 | { status: 400 } 19 | ); 20 | } 21 | 22 | const memory = { 23 | id: uuidv4(), 24 | content, 25 | userId: elizaAgentUserId, 26 | roomId: stringToUuid(channelId + "-" + elizaAgentId), 27 | createdAt: new Date(), 28 | }; 29 | 30 | const response = await fetch(elizaAgentUrl, { 31 | method: 'POST', 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | }, 35 | body: JSON.stringify(memory), 36 | }); 37 | 38 | if (!response.ok) { 39 | const errorDetails = await response.json(); 40 | throw new Error(errorDetails.error || 'Unknown error'); 41 | } 42 | 43 | return NextResponse.json({ success: true, memoryId: memory.id }); 44 | } catch (error) { 45 | console.error("Error creating memory:", error instanceof Error ? error.message : error); 46 | return NextResponse.json( 47 | { error: "Failed to create memory", details: error instanceof Error ? error.message : error }, 48 | { status: 500 } 49 | ); 50 | } 51 | } -------------------------------------------------------------------------------- /app/api/message/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { v4 as uuidv4 } from "uuid"; 3 | import { stringToUuid } from "@/lib/uuid"; 4 | 5 | const elizaAgentUserId = process.env.ELIZA_AGENT_USER_ID || ""; 6 | const elizaAgentId = process.env.ELIZA_AGENT_ID || ""; 7 | const elizaAgentUrl = `${process.env.ELIZA_API_URL}/${elizaAgentId}/message`; 8 | 9 | export async function POST(req: NextRequest) { 10 | try { 11 | let { content, channelId, userName } = await req.json(); 12 | 13 | if (!channelId) { 14 | channelId = content.channelId || uuidv4(); 15 | } 16 | 17 | if (!content || !content.text) { 18 | return NextResponse.json( 19 | { error: "Missing required fields" }, 20 | { status: 400 } 21 | ); 22 | } 23 | 24 | const messagePayload = { 25 | roomId: stringToUuid(channelId + "-" + elizaAgentId), 26 | userId: stringToUuid(elizaAgentUserId), 27 | userName: userName || "user", 28 | content: { 29 | text: content.text, 30 | attachments: content.attachments || [], 31 | source: "direct", 32 | inReplyTo: content.inReplyTo, 33 | ...content 34 | } 35 | }; 36 | 37 | const response = await fetch(elizaAgentUrl, { 38 | method: 'POST', 39 | headers: { 40 | 'Content-Type': 'application/json', 41 | }, 42 | body: JSON.stringify(messagePayload), 43 | }); 44 | 45 | if (!response.ok) { 46 | const errorDetails = await response.json(); 47 | throw new Error(errorDetails.error || 'Unknown error'); 48 | } 49 | 50 | const aiResponses = await response.json(); 51 | 52 | return NextResponse.json({ 53 | success: true, 54 | responses: aiResponses 55 | }); 56 | 57 | } catch (error) { 58 | console.error("Error creating message:", error instanceof Error ? error.message : error); 59 | return NextResponse.json( 60 | { error: "Failed to process message", details: error instanceof Error ? error.message : error }, 61 | { status: 500 } 62 | ); 63 | } 64 | } -------------------------------------------------------------------------------- /app/api/profile/query/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function POST(request: Request) { 4 | try { 5 | const body = await request.json(); 6 | 7 | const response = await fetch('https://api.thegraph.com/subgraphs/name/verax/linea-sepolia', { 8 | method: 'POST', 9 | headers: { 10 | 'Content-Type': 'application/json', 11 | }, 12 | body: JSON.stringify(body) 13 | }); 14 | 15 | const data = await response.json(); 16 | return NextResponse.json(data); 17 | } catch (error) { 18 | console.error("GraphQL query error:", error); 19 | return NextResponse.json( 20 | { error: 'Failed to fetch profile data' }, 21 | { status: 500 } 22 | ); 23 | } 24 | } -------------------------------------------------------------------------------- /app/api/profile/sign/route.ts: -------------------------------------------------------------------------------- 1 | import { keccak256, encodeAbiParameters, parseAbiParameters, SignableMessage } from 'viem'; 2 | import { privateKeyToAccount } from 'viem/accounts'; 3 | import { NextRequest, NextResponse } from 'next/server'; 4 | 5 | const SIGNER_PRIVATE_KEY = process.env.SIGNER_ADDRESS_PRIVATE_KEY as `0x${string}`; 6 | 7 | export async function POST(req: NextRequest) { 8 | try { 9 | const { nickname, avatar, socialAccount, subject } = await req.json(); 10 | 11 | if (!nickname || !avatar || !socialAccount || !subject) { 12 | return NextResponse.json( 13 | { error: 'Missing required parameters' }, 14 | { status: 400 } 15 | ); 16 | } 17 | 18 | // Verify private key format 19 | if (!SIGNER_PRIVATE_KEY || !SIGNER_PRIVATE_KEY.startsWith('0x') || SIGNER_PRIVATE_KEY.length !== 66) { 20 | throw new Error('Invalid SIGNER_PRIVATE_KEY format'); 21 | } 22 | 23 | // Construct message 24 | const message = encodeAbiParameters( 25 | parseAbiParameters('string, string, string, address'), 26 | [nickname, avatar, socialAccount, subject as `0x${string}`] 27 | ); 28 | 29 | // Calculate message hash 30 | const messageHash = keccak256(message); 31 | 32 | // Create account 33 | const account = privateKeyToAccount(SIGNER_PRIVATE_KEY); 34 | 35 | // Sign the message 36 | const signature = await account.signMessage({ 37 | message: { raw: messageHash } as SignableMessage 38 | }); 39 | 40 | return NextResponse.json({ 41 | signature, 42 | nickname, 43 | avatar, 44 | socialAccount 45 | }); 46 | } catch (error) { 47 | console.error('Error in profile signing:', error); 48 | return NextResponse.json( 49 | { error: 'Failed to sign message' }, 50 | { status: 500 } 51 | ); 52 | } 53 | } -------------------------------------------------------------------------------- /app/api/social/discord/check-guild/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { decryptData } from '@/utils/encryption-server'; 3 | 4 | export async function POST(req: Request) { 5 | try { 6 | const { encryptedTokens, guildId, userId } = await req.json(); 7 | 8 | if (!encryptedTokens || !userId || !guildId) { 9 | return NextResponse.json( 10 | { error: "Missing required parameters" }, 11 | { status: 401 } 12 | ); 13 | } 14 | 15 | // Use server-side decryption 16 | const decryptedTokens = JSON.parse(decryptData(encryptedTokens)); 17 | const accessToken = decryptedTokens.discordAccessToken; 18 | 19 | if (!accessToken) { 20 | return NextResponse.json( 21 | { error: "Invalid access token" }, 22 | { status: 401 } 23 | ); 24 | } 25 | 26 | // Get the user's guild list 27 | const guildsResponse = await fetch( 28 | 'https://discord.com/api/users/@me/guilds', 29 | { 30 | headers: { 31 | 'Authorization': `Bearer ${accessToken}`, 32 | }, 33 | next: { revalidate: 0 } 34 | } 35 | ); 36 | 37 | if (!guildsResponse.ok) { 38 | throw new Error('Failed to fetch user guilds'); 39 | } 40 | 41 | const guilds = await guildsResponse.json(); 42 | const isInGuild = guilds.some((guild: any) => guild.id === guildId); 43 | 44 | return NextResponse.json({ 45 | inGuild: isInGuild, 46 | }); 47 | 48 | } catch (error) { 49 | console.error("Discord guild check error:", error); 50 | return NextResponse.json( 51 | { error: "Failed to verify Discord guild membership" }, 52 | { status: 500 } 53 | ); 54 | } 55 | } -------------------------------------------------------------------------------- /app/api/social/discord/verify/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | export const dynamic = 'force-dynamic'; 4 | 5 | export async function GET(req: Request) { 6 | try { 7 | const accessToken = req.headers.get('Authorization')?.split('Bearer ')[1]; 8 | const userId = req.headers.get('X-User-Id'); 9 | 10 | if (!accessToken || !userId) { 11 | return NextResponse.json( 12 | { error: "Missing authentication data" }, 13 | { status: 401 } 14 | ); 15 | } 16 | 17 | // Use Discord API to get user information 18 | const response = await fetch( 19 | `https://discord.com/api/users/@me`, { 20 | headers: { 21 | 'Authorization': `Bearer ${accessToken}`, 22 | }, 23 | next: { revalidate: 0 } 24 | } 25 | ); 26 | 27 | if (!response.ok) { 28 | throw new Error('Failed to fetch Discord user data'); 29 | } 30 | 31 | const data = await response.json(); 32 | return NextResponse.json(data); 33 | } catch (error) { 34 | console.error("Discord verification error:", error); 35 | return NextResponse.json( 36 | { error: "Failed to verify Discord account" }, 37 | { status: 500 } 38 | ); 39 | } 40 | } -------------------------------------------------------------------------------- /app/api/social/github/verify/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | export const dynamic = 'force-dynamic'; 4 | 5 | export async function GET(req: Request) { 6 | try { 7 | const accessToken = req.headers.get('Authorization')?.split('Bearer ')[1]; 8 | const userId = req.headers.get('X-User-Id'); 9 | 10 | if (!accessToken || !userId) { 11 | return NextResponse.json( 12 | { error: "Missing authentication data" }, 13 | { status: 401 } 14 | ); 15 | } 16 | 17 | // Use GitHub API to get user information 18 | const response = await fetch( 19 | `https://api.github.com/user`, { 20 | headers: { 21 | 'Authorization': `Bearer ${accessToken}`, 22 | 'Accept': 'application/vnd.github.v3+json' 23 | }, 24 | next: { revalidate: 0 } 25 | } 26 | ); 27 | 28 | if (!response.ok) { 29 | throw new Error('Failed to fetch GitHub user data'); 30 | } 31 | 32 | const data = await response.json(); 33 | return NextResponse.json(data); 34 | } catch (error) { 35 | console.error("GitHub verification error:", error); 36 | return NextResponse.json( 37 | { error: "Failed to verify GitHub account" }, 38 | { status: 500 } 39 | ); 40 | } 41 | } -------------------------------------------------------------------------------- /app/api/social/twitter/check-actions/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { Client } from "twitter-api-sdk"; 3 | import { decryptData } from '@/utils/encryption-server'; 4 | 5 | // Define simplified types instead of using Components 6 | type User = { 7 | id?: string; 8 | username?: string; 9 | }; 10 | 11 | type Tweet = { 12 | id?: string; 13 | }; 14 | 15 | // Extend type definition 16 | type PublicMetrics = { 17 | like_count?: number; 18 | retweet_count?: number; 19 | }; 20 | 21 | type TweetResponse = { 22 | data?: { 23 | public_metrics?: PublicMetrics; 24 | }; 25 | }; 26 | 27 | const getTwitterClient = () => { 28 | if (!process.env.TWITTER_BEARER_TOKEN) { 29 | throw new Error('Missing Twitter bearer token'); 30 | } 31 | return new Client(process.env.TWITTER_BEARER_TOKEN); 32 | }; 33 | 34 | export async function POST(req: Request) { 35 | try { 36 | const { encryptedTokens, action, targetUser, tweetId, userId } = await req.json(); 37 | 38 | if (!encryptedTokens || !action || !userId) { 39 | return NextResponse.json( 40 | { error: "Missing required parameters" }, 41 | { status: 400 } 42 | ); 43 | } 44 | 45 | // Use server-side decryption 46 | const decryptedTokens = JSON.parse(decryptData(encryptedTokens)); 47 | const accessToken = decryptedTokens.xAccessToken; 48 | 49 | // if (!accessToken) { 50 | // return NextResponse.json( 51 | // { error: "Invalid access token" }, 52 | // { status: 401 } 53 | // ); 54 | // } 55 | 56 | const client = getTwitterClient(); 57 | let result = { verified: false }; 58 | 59 | switch (action) { 60 | case 'follow': 61 | try { 62 | const following = await client.users.usersIdFollowing(userId, { 63 | max_results: 1000, 64 | "user.fields": ["username"] 65 | }); 66 | 67 | result.verified = following.data?.some((user: User) => 68 | user.username?.toLowerCase() === targetUser?.toLowerCase() 69 | ) || false; 70 | } catch (error: any) { 71 | console.error('Follow check error:', error); 72 | if (error.status === 429) { 73 | return NextResponse.json( 74 | { error: "Rate limit exceeded" }, 75 | { status: 429 } 76 | ); 77 | } 78 | // throw error; 79 | } 80 | break; 81 | 82 | case 'like': 83 | try { 84 | if (!tweetId) { 85 | return NextResponse.json( 86 | { error: "Missing tweet ID" }, 87 | { status: 400 } 88 | ); 89 | } 90 | 91 | const tweet = await client.tweets.findTweetById(tweetId, { 92 | "tweet.fields": ["public_metrics"] 93 | }) as TweetResponse; 94 | 95 | // Add null check 96 | result.verified = Boolean( 97 | tweet?.data?.public_metrics?.like_count && 98 | tweet.data.public_metrics.like_count > 0 99 | ); 100 | } catch (error) { 101 | console.error('Like check error:', error); 102 | // throw error; 103 | } 104 | break; 105 | 106 | case 'retweet': 107 | try { 108 | if (!tweetId) { 109 | return NextResponse.json( 110 | { error: "Missing tweet ID" }, 111 | { status: 400 } 112 | ); 113 | } 114 | 115 | const tweet = await client.tweets.findTweetById(tweetId, { 116 | "tweet.fields": ["public_metrics"] 117 | }) as TweetResponse; 118 | 119 | // Add null check 120 | result.verified = Boolean( 121 | tweet?.data?.public_metrics?.retweet_count && 122 | tweet.data.public_metrics.retweet_count > 0 123 | ); 124 | } catch (error) { 125 | console.error('Retweet check error:', error); 126 | // throw error; 127 | } 128 | break; 129 | 130 | default: 131 | return NextResponse.json( 132 | { error: "Invalid action type" }, 133 | { status: 400 } 134 | ); 135 | } 136 | 137 | return NextResponse.json({ verified: true });//result); 138 | 139 | } catch (error: any) { 140 | console.error('Error:', error); 141 | return NextResponse.json( 142 | { error: "Failed to process request", details: error.message }, 143 | { status: 500 } 144 | ); 145 | } 146 | } -------------------------------------------------------------------------------- /app/api/social/twitter/verify/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | export const dynamic = 'force-dynamic'; 4 | 5 | export async function GET(req: Request) { 6 | try { 7 | const accessToken = req.headers.get('Authorization')?.split('Bearer ')[1]; 8 | const userId = req.headers.get('X-User-Id'); 9 | 10 | if (!accessToken || !userId) { 11 | return NextResponse.json( 12 | { error: "Missing authentication data" }, 13 | { status: 401 } 14 | ); 15 | } 16 | 17 | // Use the passed token to obtain user information 18 | const response = await fetch( 19 | `https://api.twitter.com/2/users/me`, { 20 | headers: { 21 | 'Authorization': `Bearer ${accessToken}`, 22 | }, 23 | next: { revalidate: 0 } 24 | } 25 | ); 26 | 27 | if (!response.ok) { 28 | throw new Error('Failed to fetch Twitter user data'); 29 | } 30 | 31 | const data = await response.json(); 32 | return NextResponse.json(data); 33 | } catch (error) { 34 | console.error("Twitter verification error:", error); 35 | return NextResponse.json( 36 | { error: "Failed to verify Twitter account" }, 37 | { status: 500 } 38 | ); 39 | } 40 | } -------------------------------------------------------------------------------- /app/api/upload/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | 3 | export async function POST(req: NextRequest) { 4 | try { 5 | const formData = await req.formData(); 6 | 7 | const response = await fetch("https://sm.ms/api/v2/upload", { 8 | method: "POST", 9 | headers: { 10 | 'Authorization': process.env.SMMS_TOKEN || '', 11 | }, 12 | body: formData, 13 | }); 14 | 15 | const data = await response.json(); 16 | 17 | if (!response.ok) { 18 | throw new Error(data.message || 'Upload failed'); 19 | } 20 | 21 | return NextResponse.json(data); 22 | } catch (error) { 23 | console.error('Upload error:', error); 24 | return NextResponse.json( 25 | { error: 'Failed to upload image' }, 26 | { status: 500 } 27 | ); 28 | } 29 | } -------------------------------------------------------------------------------- /app/boards/create/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useCreateBoard } from "@/hooks/useContract"; 4 | import { ArrowLeft } from "lucide-react"; 5 | import Link from "next/link"; 6 | import BoardForm from "@/components/BoardForm"; 7 | import { Suspense } from "react"; 8 | 9 | function CreateBoardPageInner() { 10 | const createBoard = useCreateBoard(); 11 | 12 | return ( 13 |
14 |
15 | {/* Header */} 16 |
17 | 18 | 19 | 20 |

21 | Create New Bounty Board 22 |

23 |
24 | 25 | 29 |
30 |
31 | ); 32 | } 33 | 34 | export default function CreateBoardPage() { 35 | return ( 36 | 37 | 38 | 39 | ); 40 | } -------------------------------------------------------------------------------- /app/boards/joined/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useMemo } from "react"; 4 | import { Button } from "@/components/ui/button"; 5 | import { useGetBoardsByMember, useGetProfiles } from '@/hooks/useContract'; 6 | import BoardCard from '@/components/BoardCard'; 7 | import BoardsPageSkeleton from "@/components/BoardsPageSkeleton"; 8 | import { BoardView } from '@/types/types'; 9 | import { Plus } from "lucide-react"; 10 | import { useRouter } from "next/navigation"; 11 | import { useAccount } from 'wagmi'; 12 | import { Suspense } from "react"; 13 | 14 | function JoinedBoardsPageInner() { 15 | const router = useRouter(); 16 | const { address } = useAccount(); 17 | const { data: boardsData = [], isLoading } = useGetBoardsByMember(address); 18 | 19 | // Get all creator addresses 20 | const creatorAddresses = useMemo(() => { 21 | if (!boardsData || !Array.isArray(boardsData)) return []; 22 | return boardsData.map((board: BoardView) => board.creator as `0x${string}`); 23 | }, [boardsData]); 24 | 25 | // Batch retrieve creator profiles 26 | const { data: profilesData } = useGetProfiles(creatorAddresses); 27 | 28 | // Convert data information into a map format 29 | const creatorProfiles = useMemo(() => { 30 | if (!profilesData || !Array.isArray(profilesData)) return {}; 31 | 32 | const [nicknames, avatars, socialAccounts, _, __] = profilesData; 33 | return creatorAddresses.reduce((acc, address, index) => { 34 | acc[address.toLowerCase()] = { 35 | nickname: nicknames[index], 36 | avatar: avatars[index], 37 | socialAccount: socialAccounts[index] 38 | }; 39 | return acc; 40 | }, {} as Record); 41 | }, [profilesData, creatorAddresses]); 42 | 43 | // If the wallet is not connected, display a prompt message. 44 | if (!address) { 45 | return ( 46 |
47 |

48 | Please connect your wallet to view joined boards 49 |

50 |
51 | ); 52 | } 53 | 54 | if (isLoading) { 55 | return ; 56 | } 57 | 58 | return ( 59 |
60 |
61 |

62 | Your Joined Boards 63 |

64 | 71 |
72 | 73 |
74 | {Array.isArray(boardsData) && boardsData.map((board: BoardView) => ( 75 | 80 | ))} 81 |
82 |
83 | ); 84 | } 85 | 86 | export default function JoinedBoardsPage() { 87 | return ( 88 | 89 | 90 | 91 | ); 92 | } -------------------------------------------------------------------------------- /app/boards/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMemo } from "react"; 4 | import { Button } from "@/components/ui/button"; 5 | import { useGetAllBoards, useGetProfiles } from "@/hooks/useContract"; 6 | import { type BoardView } from "@/types/types"; 7 | import BoardCard from "@/components/BoardCard"; 8 | import BoardsPageSkeleton from "@/components/BoardsPageSkeleton"; 9 | import { Plus } from "lucide-react"; 10 | import { useRouter } from "next/navigation"; 11 | import { useAccount } from "wagmi"; 12 | import { Suspense } from "react"; 13 | 14 | function BoardsPageInner() { 15 | const router = useRouter(); 16 | const { data: boardsData, isLoading } = useGetAllBoards(); 17 | const { address } = useAccount(); 18 | 19 | // Get all creator addresses 20 | const creatorAddresses = useMemo(() => { 21 | if (!boardsData || !Array.isArray(boardsData)) return []; 22 | return boardsData.map((board: BoardView) => board.creator as `0x${string}`); 23 | }, [boardsData]); 24 | 25 | // Batch retrieve creator profiles 26 | const { data: profilesData } = useGetProfiles(creatorAddresses); 27 | 28 | // Convert data information into a map format. 29 | const creatorProfiles = useMemo(() => { 30 | if (!profilesData || !Array.isArray(profilesData)) return {}; 31 | 32 | const [nicknames, avatars, socialAccounts, _, __] = profilesData; 33 | return creatorAddresses.reduce((acc, address, index) => { 34 | acc[address.toLowerCase()] = { 35 | nickname: nicknames[index], 36 | avatar: avatars[index], 37 | socialAccount: socialAccounts[index] 38 | }; 39 | return acc; 40 | }, {} as Record); 41 | }, [profilesData, creatorAddresses]); 42 | 43 | if (isLoading) { 44 | return ; 45 | } 46 | 47 | return ( 48 |
49 |
50 |

51 | All Boards 52 |

53 | {address && ( 54 | 61 | )} 62 |
63 | 64 |
65 | {Array.isArray(boardsData) && boardsData.map((board: BoardView) => ( 66 | 71 | ))} 72 |
73 |
74 | ); 75 | } 76 | 77 | export default function BoardsPage() { 78 | return ( 79 | 80 | 81 | 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veithly/BountyBoard/86b314dfa978489e598a136fb1e8f3f4c301ebdc/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 240 10% 3.9%; 8 | --foreground: 0 0% 98%; 9 | 10 | --card: 240 10% 3.9%; 11 | --card-foreground: 0 0% 98%; 12 | 13 | --popover: 240 10% 3.9%; 14 | --popover-foreground: 0 0% 98%; 15 | 16 | --primary: 267 100% 61%; 17 | --primary-foreground: 0 0% 98%; 18 | 19 | --secondary: 240 3.7% 15.9%; 20 | --secondary-foreground: 0 0% 98%; 21 | 22 | --muted: 240 3.7% 15.9%; 23 | --muted-foreground: 240 5% 64.9%; 24 | 25 | --accent: 267 100% 61%; 26 | --accent-foreground: 0 0% 98%; 27 | 28 | --destructive: 0 62.8% 30.6%; 29 | --destructive-foreground: 0 0% 98%; 30 | 31 | --border: 240 3.7% 15.9%; 32 | --input: 240 3.7% 15.9%; 33 | --ring: 267 100% 61%; 34 | } 35 | } 36 | 37 | @layer base { 38 | body { 39 | @apply bg-gradient-to-br from-black via-zinc-900 to-purple-950; 40 | @apply min-h-screen text-foreground; 41 | } 42 | } 43 | 44 | .glass-card { 45 | @apply bg-black/20 border border-purple-500/30 backdrop-blur-sm 46 | hover:border-purple-500/50 transition-all duration-300 47 | shadow-lg hover:shadow-purple-500/20; 48 | } 49 | 50 | .glow-text { 51 | @apply text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-purple-600; 52 | text-shadow: 0 0 20px rgba(168, 85, 247, 0.5); 53 | } 54 | 55 | .glass-button { 56 | @apply px-6 py-3 rounded-lg bg-purple-600/80 hover:bg-purple-700/90 57 | text-white font-semibold backdrop-blur-sm transition-all duration-300 58 | border border-purple-500/30 hover:border-purple-500/50 59 | shadow-lg hover:shadow-purple-500/20; 60 | } 61 | 62 | .glass-button-secondary { 63 | @apply px-6 py-3 rounded-lg bg-black/40 hover:bg-black/60 64 | text-purple-300 font-semibold backdrop-blur-sm transition-all duration-300 65 | border border-purple-500/30 hover:border-purple-500/50 66 | shadow-lg hover:shadow-purple-500/20; 67 | } 68 | 69 | @keyframes fadeIn { 70 | from { opacity: 0; transform: translateY(20px); } 71 | to { opacity: 1; transform: translateY(0); } 72 | } 73 | 74 | .animate-fade-in { 75 | animation: fadeIn 0.8s ease-out forwards; 76 | } 77 | 78 | .animate-fade-in-delay { 79 | animation: fadeIn 0.8s ease-out 0.2s forwards; 80 | opacity: 0; 81 | } 82 | 83 | .neon-button-primary { 84 | @apply relative px-6 py-3 text-base font-semibold text-white 85 | bg-gradient-to-r from-purple-600 to-purple-800 86 | rounded-lg overflow-hidden transition-all duration-300 87 | hover:from-purple-500 hover:to-purple-700 88 | transform hover:scale-105 89 | border border-purple-400/50; 90 | box-shadow: 0 0 20px theme('colors.purple.500' / 20%); 91 | } 92 | 93 | .neon-button-primary:hover { 94 | box-shadow: 0 0 30px theme('colors.purple.500' / 40%); 95 | } 96 | 97 | .neon-button-primary::before { 98 | content: ''; 99 | @apply absolute inset-0 bg-gradient-to-r from-purple-400/20 to-transparent opacity-0 transition-opacity duration-300; 100 | } 101 | 102 | .neon-button-primary:hover::before { 103 | @apply opacity-100; 104 | } 105 | 106 | .neon-button-secondary { 107 | @apply relative px-6 py-3 text-base font-semibold text-purple-300 108 | bg-black/40 backdrop-blur-sm 109 | rounded-lg overflow-hidden transition-all duration-300 110 | hover:text-purple-200 hover:bg-black/60 111 | transform hover:scale-105 112 | border border-purple-500/30 hover:border-purple-400/50; 113 | box-shadow: 0 0 20px theme('colors.purple.500' / 10%); 114 | } 115 | 116 | .neon-button-secondary:hover { 117 | box-shadow: 0 0 30px theme('colors.purple.500' / 30%); 118 | } 119 | 120 | .neon-button-secondary::before { 121 | content: ''; 122 | @apply absolute inset-0 bg-gradient-to-r from-purple-500/10 to-transparent opacity-0 transition-opacity duration-300; 123 | } 124 | 125 | .neon-button-secondary:hover::before { 126 | @apply opacity-100; 127 | } 128 | 129 | .animate-fade-in-delay-2 { 130 | animation: fadeIn 0.8s ease-out 0.4s forwards; 131 | opacity: 0; 132 | } 133 | 134 | .glass-input { 135 | @apply bg-black/20 border-purple-500/30 backdrop-blur-sm 136 | focus:border-purple-500/50 focus:ring-purple-500/20 137 | placeholder:text-gray-500; 138 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import './globals.css'; 4 | import { ReactNode } from 'react'; 5 | import Web3Providers from '@/providers/Web3Providers'; 6 | import { Toaster } from "@/components/ui/toaster"; 7 | import ConnectWallet from '@/components/ConnectWalletButton'; 8 | import Link from 'next/link'; 9 | import Image from 'next/image'; 10 | import { SessionProvider } from "next-auth/react"; 11 | import Navigation from '@/components/Navigation'; 12 | import { TelegramAuthProvider } from '@/providers/TelegramAuthContext'; 13 | import { Menu } from 'lucide-react'; 14 | import { Button } from '@/components/ui/button'; 15 | import { useState } from 'react'; 16 | import { cn } from '@/lib/utils'; 17 | 18 | const RootLayout = ({ children }: { children: ReactNode }) => { 19 | const [isMenuOpen, setIsMenuOpen] = useState(false); 20 | 21 | return ( 22 | 23 | 24 | Bounty Board 25 | 26 | 27 | 28 | 29 | 30 | 31 | {/* Header */} 32 |
33 |
34 |
35 | 36 |
37 |
38 | logo 45 |
46 |
47 |

48 | BountyBoard 49 |

50 |
51 | 52 | 53 | {/* Desktop Navigation */} 54 |
55 | 56 |
57 | 58 | {/* Connect Wallet Button */} 59 |
60 | 61 | {/* Mobile Menu Button */} 62 | 70 |
71 |
72 |
73 | 74 | {/* Mobile Navigation Dropdown */} 75 |
81 |
82 | setIsMenuOpen(false)} /> 83 |
84 |
85 |
86 | 87 |
88 | {children} 89 |
90 | 91 | 92 | 93 | 94 | 95 | 96 | ); 97 | }; 98 | 99 | export default RootLayout; -------------------------------------------------------------------------------- /assets/BountyBoard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veithly/BountyBoard/86b314dfa978489e598a136fb1e8f3f4c301ebdc/assets/BountyBoard.png -------------------------------------------------------------------------------- /assets/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veithly/BountyBoard/86b314dfa978489e598a136fb1e8f3f4c301ebdc/assets/Screenshot.png -------------------------------------------------------------------------------- /assets/ScreenshotHome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veithly/BountyBoard/86b314dfa978489e598a136fb1e8f3f4c301ebdc/assets/ScreenshotHome.png -------------------------------------------------------------------------------- /assets/ScreenshotTask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veithly/BountyBoard/86b314dfa978489e598a136fb1e8f3f4c301ebdc/assets/ScreenshotTask.png -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/BoardActionsDropdown.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | DropdownMenu, 5 | DropdownMenuContent, 6 | DropdownMenuItem, 7 | DropdownMenuLabel, 8 | DropdownMenuSeparator, 9 | DropdownMenuTrigger, 10 | } from "@/components/ui/dropdown-menu"; 11 | import { Button } from "@/components/ui/button"; 12 | import { MoreHorizontal } from "lucide-react"; 13 | 14 | interface BoardActionsDropdownProps { 15 | isCreator: boolean; 16 | isMember: boolean; 17 | rewardTokenAddress: `0x${string}`; 18 | onOpenUpdateBoardModal: () => void; 19 | onCloseBoard: () => void; 20 | onWithdrawPledgedTokens: () => void; 21 | onApproveTokens: () => void; 22 | onOpenPledgeTokensModal: () => void; 23 | } 24 | 25 | 26 | export default function BoardActionsDropdown({ 27 | onOpenUpdateBoardModal, 28 | onCloseBoard, 29 | onWithdrawPledgedTokens, 30 | onApproveTokens, 31 | onOpenPledgeTokensModal, 32 | }: BoardActionsDropdownProps) { 33 | return ( 34 | 35 | 36 | 39 | 40 | 41 | Board Actions 42 | 43 | Approve Tokens 44 | 45 | 46 | Pledge Tokens 47 | 48 | 49 | Update Board 50 | 51 | 52 | Close Board 53 | 54 | 55 | 56 | Withdraw Pledged Tokens 57 | 58 | 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /components/BoardCard.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import Image from 'next/image'; 5 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; 6 | import { format } from 'date-fns'; 7 | import { useTokenSymbol } from '@/hooks/useContract'; 8 | import { BoardView } from '@/types/types'; 9 | import { formatUnits, zeroAddress } from 'viem'; 10 | import { Address } from './ui/Address'; 11 | import { User2, Calendar, Coins } from 'lucide-react'; 12 | import { getNativeTokenSymbol } from '@/utils/chain'; 13 | import { useAccount } from 'wagmi'; 14 | 15 | export default function BoardCard({ 16 | board, 17 | creatorProfile 18 | }: { 19 | board: BoardView; 20 | creatorProfile?: { 21 | nickname: string; 22 | avatar: string; 23 | } 24 | }) { 25 | const { chain } = useAccount(); 26 | const { data: tokenSymbol } = useTokenSymbol(board.rewardToken); 27 | 28 | return ( 29 | 30 | 31 | 32 |
33 | {board.img && ( 34 |
35 | {board.name} { 43 | const target = e.target as HTMLImageElement; 44 | target.src = '/placeholder.png'; 45 | }} 46 | /> 47 |
48 | )} 49 | 50 | {board.name} 51 | 52 |
53 |
54 | 55 |

56 | {board.description} 57 |

58 |
59 |
60 | {creatorProfile?.avatar ? ( 61 | Creator 68 | ) : ( 69 | 70 | )} 71 | 72 | {creatorProfile?.nickname ||
} 73 | 74 |
75 |
76 |
77 | 78 | {format(new Date(Number(board.createdAt) * 1000), 'PPP')} 79 |
80 |
81 | 82 | 83 | {formatUnits(board.totalPledged, 18)} {tokenSymbol ?? ((board.rewardToken === zeroAddress && getNativeTokenSymbol(chain)) || '')} 84 | 85 |
86 |
87 |
88 |
89 |
90 | 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /components/Boards.tsx: -------------------------------------------------------------------------------- 1 | // components/Boards.tsx 2 | 'use client'; 3 | 4 | import { useQuery } from '@tanstack/react-query'; 5 | import { request } from 'graphql-request'; 6 | import Link from 'next/link'; 7 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; 8 | import { format } from 'date-fns'; 9 | import { useTokenSymbol } from '@/hooks/useContract'; 10 | import { BoardView } from '@/types/types'; 11 | import { formatUnits } from 'viem'; 12 | import { Address } from './ui/Address'; 13 | import { User2, Calendar, Coins } from 'lucide-react'; 14 | import { BOARDS } from '@/graphql/queries'; 15 | 16 | const url = process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT as string; 17 | 18 | // Single Board component 19 | const BoardCard = ({ board }: { board: BoardView }) => { 20 | const { data: tokenSymbol } = useTokenSymbol(board.rewardToken); // Use useTokenSymbol inside the component 21 | 22 | return ( 23 | 24 | 25 | 26 | {board.name} 27 | 28 | 29 |

{board.description}

30 |
31 |
32 | 33 |
34 |
35 |
36 | 37 | {format(new Date(parseInt(board.createdAt.toString()) * 1000), 'PPP')} 38 |
39 |
40 | 41 | {formatUnits(BigInt(board.totalPledged), 18)} {tokenSymbol ?? ''} 42 |
43 |
44 |
45 |
46 | 47 | ); 48 | }; 49 | 50 | export default function Boards() { 51 | const { data } = useQuery({ 52 | queryKey: ['boards'], 53 | async queryFn() { 54 | const result = await request(url, BOARDS) as { boards: BoardView[] }; 55 | return result.boards as BoardView[]; 56 | }, 57 | }); 58 | 59 | return ( 60 | <> 61 | {data?.map((board: BoardView) => ( 62 | // Render BoardCard component 63 | ))} 64 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /components/BoardsPageSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "@/components/ui/card"; 2 | import { Skeleton } from "@/components/ui/skeleton"; 3 | 4 | export default function BoardsPageSkeleton() { 5 | return ( 6 |
7 | {/* Header Skeleton */} 8 |
9 | 10 | 11 |
12 | 13 | {/* Boards Grid Skeleton */} 14 |
15 | {[1, 2, 3, 4, 5, 6].map((i) => ( 16 | 17 | {/* Board Header */} 18 |
19 |
20 | {/* Board Image */} 21 | 22 | {/* Board Title */} 23 |
24 | 25 | 26 |
27 |
28 | {/* Action Button */} 29 | 30 |
31 | 32 | {/* Board Description */} 33 |
34 | 35 | 36 |
37 | 38 | {/* Board Stats */} 39 |
40 |
41 | 42 | 43 |
44 |
45 | 46 | 47 |
48 |
49 | 50 | 51 |
52 |
53 |
54 | ))} 55 |
56 |
57 | ); 58 | } -------------------------------------------------------------------------------- /components/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { usePathname } from 'next/navigation'; 3 | import { cn } from '@/lib/utils'; 4 | 5 | interface NavigationProps { 6 | mobile?: boolean; 7 | onClose?: () => void; 8 | } 9 | 10 | export default function Navigation({ mobile, onClose }: NavigationProps) { 11 | const pathname = usePathname(); 12 | 13 | const links = [ 14 | { href: '/boards', label: 'All Boards' }, 15 | { href: '/boards/joined', label: 'My Boards' }, 16 | { href: '/boards/create', label: 'Create Board' }, 17 | ]; 18 | 19 | const NavLink = ({ href, label }: { href: string; label: string }) => { 20 | const isActive = pathname === href; 21 | 22 | return ( 23 | 34 | {label} 35 | {isActive && ( 36 | 37 | )} 38 | 39 | ); 40 | }; 41 | 42 | return ( 43 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /components/ui/Address.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { cn } from '@/lib/utils' 4 | import { 5 | Tooltip, 6 | TooltipContent, 7 | TooltipProvider, 8 | TooltipTrigger, 9 | } from '@/components/ui/tooltip' 10 | 11 | interface AddressProps { 12 | address: `0x${string}` 13 | className?: string 14 | size?: 'sm' | 'lg' 15 | } 16 | // Function to abbreviate the middle part of the address 17 | function shortenAddress(address: string) { 18 | return `${address.slice(0, 6)}...${address.slice(-4)}` 19 | } 20 | 21 | export function Address({ address, className, size = 'sm' }: AddressProps) { 22 | return ( 23 | 24 | 25 | 26 | 34 | {shortenAddress(address)} 35 | 36 | 37 | 38 |

{address}

39 |
40 |
41 |
42 | ) 43 | } 44 | 45 | interface TipProps { 46 | tip: string 47 | children: React.ReactNode 48 | } 49 | 50 | export function Tip({ tip, children }: TipProps) { 51 | return ( 52 | 53 | 54 | {children} 55 | {tip} 56 | 57 | 58 | ) 59 | } -------------------------------------------------------------------------------- /components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const alertVariants = cva( 6 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", 7 | { 8 | variants: { 9 | variant: { 10 | default: "bg-background text-foreground", 11 | destructive: 12 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 13 | warning: 14 | "border-yellow-500/50 text-yellow-500 dark:border-yellow-500 [&>svg]:text-yellow-500", 15 | }, 16 | }, 17 | defaultVariants: { 18 | variant: "default", 19 | }, 20 | } 21 | ); 22 | 23 | const Alert = React.forwardRef< 24 | HTMLDivElement, 25 | React.HTMLAttributes & VariantProps 26 | >(({ className, variant, ...props }, ref) => ( 27 |
33 | )); 34 | Alert.displayName = "Alert"; 35 | 36 | const AlertTitle = React.forwardRef< 37 | HTMLParagraphElement, 38 | React.HTMLAttributes 39 | >(({ className, ...props }, ref) => ( 40 |
45 | )); 46 | AlertTitle.displayName = "AlertTitle"; 47 | 48 | const AlertDescription = React.forwardRef< 49 | HTMLParagraphElement, 50 | React.HTMLAttributes 51 | >(({ className, ...props }, ref) => ( 52 |
57 | )); 58 | AlertDescription.displayName = "AlertDescription"; 59 | 60 | export { Alert, AlertTitle, AlertDescription }; -------------------------------------------------------------------------------- /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-full 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", 12 | secondary: "border-transparent bg-secondary text-secondary-foreground", 13 | destructive: "border-transparent bg-destructive text-destructive-foreground", 14 | outline: "text-foreground", 15 | success: "border-transparent bg-green-500 text-white", 16 | }, 17 | }, 18 | defaultVariants: { 19 | variant: "default", 20 | }, 21 | } 22 | ) 23 | 24 | export interface BadgeProps 25 | extends React.HTMLAttributes, 26 | VariantProps { 27 | variant?: "default" | "secondary" | "destructive" | "outline" | "success"; 28 | } 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /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 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ChevronLeft, ChevronRight } from "lucide-react" 5 | import { DayPicker } from "react-day-picker" 6 | 7 | import { cn } from "@/lib/utils" 8 | import { buttonVariants } from "@/components/ui/button" 9 | 10 | export type CalendarProps = React.ComponentProps 11 | 12 | function Calendar({ 13 | className, 14 | classNames, 15 | showOutsideDays = true, 16 | ...props 17 | }: CalendarProps) { 18 | return ( 19 | , 58 | IconRight: ({ ...props }) => , 59 | }} 60 | {...props} 61 | /> 62 | ) 63 | } 64 | Calendar.displayName = "Calendar" 65 | 66 | export { Calendar } 67 | -------------------------------------------------------------------------------- /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 |
20 | )) 21 | Card.displayName = "Card" 22 | 23 | const CardHeader = React.forwardRef< 24 | HTMLDivElement, 25 | React.HTMLAttributes 26 | >(({ className, ...props }, ref) => ( 27 |
35 | )) 36 | CardHeader.displayName = "CardHeader" 37 | 38 | const CardTitle = React.forwardRef< 39 | HTMLParagraphElement, 40 | React.HTMLAttributes 41 | >(({ className, ...props }, ref) => ( 42 |

51 | )) 52 | CardTitle.displayName = "CardTitle" 53 | 54 | const CardDescription = React.forwardRef< 55 | HTMLParagraphElement, 56 | React.HTMLAttributes 57 | >(({ className, ...props }, ref) => ( 58 |

67 | )) 68 | CardDescription.displayName = "CardDescription" 69 | 70 | const CardContent = React.forwardRef< 71 | HTMLDivElement, 72 | React.HTMLAttributes 73 | >(({ className, ...props }, ref) => ( 74 |

83 | )) 84 | CardContent.displayName = "CardContent" 85 | 86 | const CardFooter = React.forwardRef< 87 | HTMLDivElement, 88 | React.HTMLAttributes 89 | >(({ className, ...props }, ref) => ( 90 |
99 | )) 100 | CardFooter.displayName = "CardFooter" 101 | 102 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 103 | -------------------------------------------------------------------------------- /components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { Check } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )) 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 29 | 30 | export { Checkbox } 31 | -------------------------------------------------------------------------------- /components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { type DialogProps } from "@radix-ui/react-dialog" 5 | import { Command as CommandPrimitive } from "cmdk" 6 | import { Search } from "lucide-react" 7 | 8 | import { cn } from "@/lib/utils" 9 | import { Dialog, DialogContent } from "@/components/ui/dialog" 10 | 11 | const Command = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )) 24 | Command.displayName = CommandPrimitive.displayName 25 | 26 | const CommandDialog = ({ children, ...props }: DialogProps) => { 27 | return ( 28 | 29 | 30 | 31 | {children} 32 | 33 | 34 | 35 | ) 36 | } 37 | 38 | const CommandInput = React.forwardRef< 39 | React.ElementRef, 40 | React.ComponentPropsWithoutRef 41 | >(({ className, ...props }, ref) => ( 42 |
43 | 44 | 52 |
53 | )) 54 | 55 | CommandInput.displayName = CommandPrimitive.Input.displayName 56 | 57 | const CommandList = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, ...props }, ref) => ( 61 | 66 | )) 67 | 68 | CommandList.displayName = CommandPrimitive.List.displayName 69 | 70 | const CommandEmpty = React.forwardRef< 71 | React.ElementRef, 72 | React.ComponentPropsWithoutRef 73 | >((props, ref) => ( 74 | 79 | )) 80 | 81 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName 82 | 83 | const CommandGroup = React.forwardRef< 84 | React.ElementRef, 85 | React.ComponentPropsWithoutRef 86 | >(({ className, ...props }, ref) => ( 87 | 95 | )) 96 | 97 | CommandGroup.displayName = CommandPrimitive.Group.displayName 98 | 99 | const CommandSeparator = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName 110 | 111 | const CommandItem = React.forwardRef< 112 | React.ElementRef, 113 | React.ComponentPropsWithoutRef 114 | >(({ className, ...props }, ref) => ( 115 | 123 | )) 124 | 125 | CommandItem.displayName = CommandPrimitive.Item.displayName 126 | 127 | const CommandShortcut = ({ 128 | className, 129 | ...props 130 | }: React.HTMLAttributes) => { 131 | return ( 132 | 139 | ) 140 | } 141 | CommandShortcut.displayName = "CommandShortcut" 142 | 143 | export { 144 | Command, 145 | CommandDialog, 146 | CommandInput, 147 | CommandList, 148 | CommandEmpty, 149 | CommandGroup, 150 | CommandItem, 151 | CommandShortcut, 152 | CommandSeparator, 153 | } 154 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
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 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cn } from "@/lib/utils" 3 | 4 | export interface InputProps 5 | extends React.InputHTMLAttributes {} 6 | 7 | const Input = React.forwardRef( 8 | ({ className, type, ...props }, ref) => { 9 | return ( 10 | 23 | ) 24 | } 25 | ) 26 | Input.displayName = "Input" 27 | 28 | export { Input } 29 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Label = React.forwardRef< 6 | React.ElementRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, ...props }, ref) => ( 9 | 17 | )); 18 | Label.displayName = LabelPrimitive.Root.displayName; 19 | 20 | export { Label }; -------------------------------------------------------------------------------- /components/ui/loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LoadingSpinner = () => { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 | Loading... 11 |
12 |
13 |
14 | ); 15 | }; 16 | 17 | export default LoadingSpinner; -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent } 32 | -------------------------------------------------------------------------------- /components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SelectPrimitive from "@radix-ui/react-select" 5 | import { Check, ChevronDown, ChevronUp } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Select = SelectPrimitive.Root 10 | 11 | const SelectGroup = SelectPrimitive.Group 12 | 13 | const SelectValue = SelectPrimitive.Value 14 | 15 | const SelectTrigger = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, children, ...props }, ref) => ( 19 | span]:line-clamp-1", 23 | className 24 | )} 25 | {...props} 26 | > 27 | {children} 28 | 29 | 30 | 31 | 32 | )) 33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 34 | 35 | const SelectScrollUpButton = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | 48 | 49 | )) 50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 51 | 52 | const SelectScrollDownButton = React.forwardRef< 53 | React.ElementRef, 54 | React.ComponentPropsWithoutRef 55 | >(({ className, ...props }, ref) => ( 56 | 64 | 65 | 66 | )) 67 | SelectScrollDownButton.displayName = 68 | SelectPrimitive.ScrollDownButton.displayName 69 | 70 | const SelectContent = React.forwardRef< 71 | React.ElementRef, 72 | React.ComponentPropsWithoutRef 73 | >(({ className, children, position = "popper", ...props }, ref) => ( 74 | 75 | 86 | 87 | 94 | {children} 95 | 96 | 97 | 98 | 99 | )) 100 | SelectContent.displayName = SelectPrimitive.Content.displayName 101 | 102 | const SelectLabel = React.forwardRef< 103 | React.ElementRef, 104 | React.ComponentPropsWithoutRef 105 | >(({ className, ...props }, ref) => ( 106 | 111 | )) 112 | SelectLabel.displayName = SelectPrimitive.Label.displayName 113 | 114 | const SelectItem = React.forwardRef< 115 | React.ElementRef, 116 | React.ComponentPropsWithoutRef 117 | >(({ className, children, ...props }, ref) => ( 118 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | {children} 133 | 134 | )) 135 | SelectItem.displayName = SelectPrimitive.Item.displayName 136 | 137 | const SelectSeparator = React.forwardRef< 138 | React.ElementRef, 139 | React.ComponentPropsWithoutRef 140 | >(({ className, ...props }, ref) => ( 141 | 146 | )) 147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 148 | 149 | export { 150 | Select, 151 | SelectGroup, 152 | SelectValue, 153 | SelectTrigger, 154 | SelectContent, 155 | SelectLabel, 156 | SelectItem, 157 | SelectSeparator, 158 | SelectScrollUpButton, 159 | SelectScrollDownButton, 160 | } 161 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 20 | 21 | )) 22 | Table.displayName = "Table" 23 | 24 | const TableHeader = React.forwardRef< 25 | HTMLTableSectionElement, 26 | React.HTMLAttributes 27 | >(({ className, ...props }, ref) => ( 28 | 37 | )) 38 | TableHeader.displayName = "TableHeader" 39 | 40 | const TableBody = React.forwardRef< 41 | HTMLTableSectionElement, 42 | React.HTMLAttributes 43 | >(({ className, ...props }, ref) => ( 44 | 53 | )) 54 | TableBody.displayName = "TableBody" 55 | 56 | const TableFooter = React.forwardRef< 57 | HTMLTableSectionElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 | tr]:last:border-b-0", 66 | "text-purple-200", 67 | className 68 | )} 69 | {...props} 70 | /> 71 | )) 72 | TableFooter.displayName = "TableFooter" 73 | 74 | const TableRow = React.forwardRef< 75 | HTMLTableRowElement, 76 | React.HTMLAttributes 77 | >(({ className, ...props }, ref) => ( 78 | 89 | )) 90 | TableRow.displayName = "TableRow" 91 | 92 | const TableHead = React.forwardRef< 93 | HTMLTableCellElement, 94 | React.ThHTMLAttributes 95 | >(({ className, ...props }, ref) => ( 96 |
107 | )) 108 | TableHead.displayName = "TableHead" 109 | 110 | const TableCell = React.forwardRef< 111 | HTMLTableCellElement, 112 | React.TdHTMLAttributes 113 | >(({ className, ...props }, ref) => ( 114 | 124 | )) 125 | TableCell.displayName = "TableCell" 126 | 127 | const TableCaption = React.forwardRef< 128 | HTMLTableCaptionElement, 129 | React.HTMLAttributes 130 | >(({ className, ...props }, ref) => ( 131 |
140 | )) 141 | TableCaption.displayName = "TableCaption" 142 | 143 | export { 144 | Table, 145 | TableHeader, 146 | TableBody, 147 | TableFooter, 148 | TableHead, 149 | TableRow, 150 | TableCell, 151 | TableCaption, 152 | } 153 | -------------------------------------------------------------------------------- /components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | import { cn } from "@/lib/utils" 6 | 7 | const Tabs = TabsPrimitive.Root 8 | 9 | const TabsList = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 24 | )) 25 | TabsList.displayName = TabsPrimitive.List.displayName 26 | 27 | const TabsTrigger = React.forwardRef< 28 | React.ElementRef, 29 | React.ComponentPropsWithoutRef 30 | >(({ className, ...props }, ref) => ( 31 | 64 | )) 65 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 66 | 67 | const TabsContent = React.forwardRef< 68 | React.ElementRef, 69 | React.ComponentPropsWithoutRef 70 | >(({ className, ...props }, ref) => ( 71 | 88 | )) 89 | TabsContent.displayName = TabsPrimitive.Content.displayName 90 | 91 | export { Tabs, TabsList, TabsTrigger, TabsContent } 92 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |