├── README.md ├── architecture.png ├── archiver ├── .gitignore ├── Dockerfile ├── README.md ├── bun.lockb ├── docker-compose.yml ├── index.ts ├── package.json ├── prisma │ └── schema.prisma ├── redisClient.ts └── tsconfig.json ├── backend ├── .gitignore ├── Dockerfile ├── README.md ├── bun.lockb ├── docker-compose.yml ├── index.ts ├── package.json ├── router │ ├── event.ts │ └── user.ts ├── services │ └── redisClient.ts ├── tsconfig.json └── utils │ ├── cashfree.ts │ └── db.ts ├── client ├── .dockerignore ├── .env.example ├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── actions │ ├── balance │ │ ├── balance.ts │ │ └── order.ts │ ├── events │ │ └── event.ts │ ├── portfolio │ │ └── portfolio.ts │ └── recharge │ │ └── recharge.ts ├── app │ ├── (lobby) │ │ ├── auth │ │ │ └── signin │ │ │ │ └── page.tsx │ │ ├── event │ │ │ ├── [id] │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── recharge │ │ │ │ └── page.tsx │ │ └── portfolio │ │ │ └── page.tsx │ ├── _provider.tsx │ ├── api │ │ └── auth │ │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── components │ │ ├── EventList.tsx │ │ └── Home.tsx │ ├── favicon.ico │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── bun.lockb ├── components.json ├── components │ ├── landing │ │ ├── Appbar.tsx │ │ ├── DepositForm.tsx │ │ ├── FAQs.tsx │ │ ├── Hero.tsx │ │ ├── Navmenu.tsx │ │ ├── Orderbook.tsx │ │ ├── Portfolio.tsx │ │ ├── ProfileHeader.tsx │ │ ├── Singin.tsx │ │ └── TakesCare.tsx │ ├── theme-provider.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── chart.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── line-chart.tsx │ │ ├── mode-toggle.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── takes-care.tsx │ │ ├── toast.tsx │ │ └── toaster.tsx ├── hooks │ ├── use-cashfree.ts │ └── use-toast.ts ├── lib │ ├── auth.ts │ └── utils.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prisma.ts ├── prisma │ ├── migrations │ │ ├── 20241019102958_schema_update │ │ │ └── migration.sql │ │ ├── 20241019103907_added_locked_balances │ │ │ └── migration.sql │ │ ├── 20241020110010_ │ │ │ └── migration.sql │ │ ├── 20241027201332_added_unique_id │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── public │ ├── hero-bg.svg │ ├── login.png │ └── trading.png ├── tailwind.config.ts └── tsconfig.json ├── db.json ├── docker-compose.yml ├── orderbook ├── .gitignore ├── Dockerfile ├── README.md ├── bun.lockb ├── index.ts ├── package.json ├── prisma │ ├── migrations │ │ ├── 20241022194710_trade_schema │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── router │ └── order.ts ├── service │ ├── crashService.ts │ ├── events.ts │ ├── exit.ts │ ├── intialiseOrder.ts │ ├── redisClient.ts │ ├── s3.ts │ ├── userAuth.ts │ └── userBalance.ts ├── tsconfig.json └── utils │ └── global.ts └── websocket ├── .gitignore ├── Dockerfile ├── README.md ├── bun.lockb ├── global.ts ├── index.ts ├── package.json ├── tsconfig.json └── websocket.ts /README.md: -------------------------------------------------------------------------------- 1 | This project is a real-time opinion trading platform where users can place bets or opinions on different events, similar to prediction markets.
2 | 3 | 4 | ![Architecture Diagram](https://github.com/aryanpachori/opinion_trading/blob/main/architecture.png) 5 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryanpachori/opinion_trading/d31cf86d2394c47d739e1a9216cd60b613d93e8e/architecture.png -------------------------------------------------------------------------------- /archiver/.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /archiver/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM node:20 3 | 4 | WORKDIR /app 5 | COPY package.json bun.lockb ./ 6 | RUN npm install -g bun 7 | RUN bun install 8 | 9 | 10 | COPY . . 11 | RUN bunx prisma generate 12 | 13 | EXPOSE 3003 14 | 15 | CMD [ "bun", "run", "index.ts" ] 16 | -------------------------------------------------------------------------------- /archiver/README.md: -------------------------------------------------------------------------------- 1 | # archiver 2 | 3 | To install dependencies: 4 | 5 | ```bash 6 | bun install 7 | ``` 8 | 9 | To run: 10 | 11 | ```bash 12 | bun run index.ts 13 | ``` 14 | 15 | This project was created using `bun init` in bun v1.1.25. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /archiver/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryanpachori/opinion_trading/d31cf86d2394c47d739e1a9216cd60b613d93e8e/archiver/bun.lockb -------------------------------------------------------------------------------- /archiver/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | ports: 9 | - "3000:3000" 10 | depends_on: 11 | - redis 12 | environment: 13 | REDIS_HOST: redis 14 | REDIS_PORT: 6379 15 | 16 | redis: 17 | image: redis:alpine 18 | ports: 19 | - "6379:6379" 20 | volumes: 21 | - redis-data:/data 22 | 23 | volumes: 24 | redis-data: 25 | -------------------------------------------------------------------------------- /archiver/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cors from "cors"; 3 | import bodyParser from "body-parser"; 4 | import { redis } from "./redisClient"; 5 | import { PrismaClient } from "@prisma/client"; 6 | 7 | const app = express(); 8 | 9 | app.use(cors()); 10 | app.use(bodyParser.json()); 11 | 12 | app.listen(3003, () => { 13 | console.log("server running on 3003"); 14 | }); 15 | 16 | await redis.connect().then(() => { 17 | console.log("connected to redis"); 18 | startArchiver(); 19 | }); 20 | 21 | const prisma = new PrismaClient(); 22 | 23 | async function startArchiver() { 24 | const eventGroup = "event_streams"; 25 | const consumerName = "archiver_consumer"; 26 | let lastId = ">"; 27 | 28 | while (true) { 29 | const message = await redis.xReadGroup( 30 | consumerName, 31 | "archiver_consumer", 32 | [{ key: eventGroup, id: lastId }], 33 | { BLOCK: 0, COUNT: 1 } 34 | ); 35 | 36 | if (message && message.length > 0) { 37 | const streamData = message[0]; 38 | if (streamData && streamData.messages.length > 0) { 39 | const messages = streamData.messages; 40 | 41 | for (const { id, message } of messages) { 42 | const messageData = JSON.parse(message.data); 43 | 44 | if (message.type == "order_creation") { 45 | console.log(message.type, message.data); 46 | const order = await prisma.order.upsert({ 47 | where: { 48 | id: messageData.id, 49 | }, 50 | update: { 51 | userId: messageData.userId, 52 | price: messageData.price, 53 | Quantity: messageData.quantity, 54 | type: messageData.type, 55 | status: messageData.status, 56 | eventId: messageData.eventId, 57 | Side: messageData.side, 58 | }, 59 | create: { 60 | id: messageData.id, 61 | userId: messageData.userId, 62 | price: messageData.price, 63 | Quantity: messageData.quantity, 64 | type: messageData.type, 65 | status: messageData.status, 66 | eventId: messageData.eventId, 67 | Side: messageData.side, 68 | }, 69 | }); 70 | console.log(order); 71 | } else if (message.type == "trade") { 72 | console.log("TRADE", message.type, message.data); 73 | const trade = await prisma.trade.upsert({ 74 | where: { 75 | id: messageData.id, 76 | }, 77 | update: { 78 | eventId: messageData.eventId, 79 | sellerId: messageData.sellerId, 80 | sellerOrderId: messageData.sellerOrder_id, 81 | buyerOrderId: messageData.buyerOrder_id, 82 | sellQty: messageData.sell_qty, 83 | buyerId: messageData.buyerId, 84 | buyQty: messageData.buy_qty, 85 | buyPrice: messageData.Buyprice, 86 | sellPrice: messageData.Sellprice, 87 | }, 88 | create: { 89 | id: messageData.id, 90 | eventId: messageData.eventId, 91 | sellerId: messageData.sellerId, 92 | sellerOrderId: messageData.sellerOrder_id, 93 | buyerOrderId: messageData.buyerOrder_id, 94 | sellQty: messageData.sell_qty, 95 | buyerId: messageData.buyerId, 96 | buyQty: messageData.buy_qty, 97 | buyPrice: messageData.Buyprice, 98 | sellPrice: messageData.Sellprice, 99 | }, 100 | }); 101 | console.log(trade); 102 | } else if (message.type == "order_update") { 103 | console.log("order_update", message.data); 104 | const order = await prisma.order.update({ 105 | where: { id: messageData.id }, 106 | data: { type: messageData.type }, 107 | }); 108 | console.log(order); 109 | } else if (message.type == "order_exit") { 110 | console.log("order_exit", message.data); 111 | const order = await prisma.order.update({ 112 | where: { id: messageData.id }, 113 | data: { status: messageData.type }, 114 | }); 115 | console.log(order); 116 | } 117 | 118 | // Acknowledge the msg after procesing 119 | await redis.xAck(eventGroup, consumerName, id); 120 | } 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /archiver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "archiver", 3 | "module": "index.ts", 4 | "type": "module", 5 | "devDependencies": { 6 | "@types/bun": "latest" 7 | }, 8 | "peerDependencies": { 9 | "typescript": "^5.0.0" 10 | }, 11 | "dependencies": { 12 | "@prisma/client": "5.21.1", 13 | "@types/cors": "^2.8.17", 14 | "@types/express": "^5.0.0", 15 | "body-parser": "^1.20.3", 16 | "cors": "^2.8.5", 17 | "express": "^4.21.1", 18 | "prisma": "^5.21.1", 19 | "redis": "^4.7.0" 20 | } 21 | } -------------------------------------------------------------------------------- /archiver/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | binaryTargets = ["native", "debian-openssl-3.0.x"] 4 | } 5 | 6 | datasource db { 7 | provider = "postgresql" 8 | url = env("DATABASE_URL") 9 | } 10 | 11 | model Event { 12 | id String @id 13 | title String 14 | description String 15 | } 16 | 17 | model Order { 18 | id String @id 19 | userId String 20 | price Float 21 | Quantity Int 22 | Side TradeSide 23 | type OrderType 24 | status OrderStatus 25 | eventId String 26 | } 27 | 28 | model Trade { 29 | id String @id 30 | buyPrice Float 31 | sellPrice Float 32 | buyQty Int 33 | sellQty Int 34 | buyerId String 35 | sellerId String 36 | buyerOrderId String 37 | sellerOrderId String 38 | createdAt DateTime @default(now()) 39 | eventId String 40 | } 41 | 42 | model User { 43 | id String @id 44 | email String @unique 45 | } 46 | 47 | enum EventStatus { 48 | ONGOING 49 | ENDED 50 | } 51 | 52 | enum OrderStatus { 53 | LIVE 54 | EXECUTED 55 | } 56 | 57 | enum OrderType { 58 | BUY 59 | SELL 60 | } 61 | 62 | enum PayoutStatus { 63 | PENDING 64 | COMPLETED 65 | FAILED 66 | } 67 | 68 | enum TradeSide { 69 | YES 70 | NO 71 | } 72 | 73 | enum TradeStatus { 74 | ACTIVE 75 | PAST 76 | } 77 | 78 | enum UserRole { 79 | ADMIN 80 | USER 81 | } 82 | -------------------------------------------------------------------------------- /archiver/redisClient.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "redis"; 2 | 3 | 4 | export const redis = createClient({ 5 | socket:{ 6 | port : 6379, 7 | host : 'redis' 8 | } 9 | }) 10 | 11 | 12 | -------------------------------------------------------------------------------- /archiver/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM node:20 3 | 4 | WORKDIR /app 5 | COPY package.json bun.lockb ./ 6 | RUN npm install -g bun 7 | RUN bun install 8 | 9 | COPY . . 10 | 11 | EXPOSE 3000 12 | 13 | CMD [ "bun", "run", "index.ts" ] 14 | 15 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # backend 2 | 3 | To install dependencies: 4 | 5 | ```bash 6 | bun install 7 | ``` 8 | 9 | To run: 10 | 11 | ```bash 12 | bun run index.ts 13 | ``` 14 | 15 | This project was created using `bun init` in bun v1.1.25. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /backend/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryanpachori/opinion_trading/d31cf86d2394c47d739e1a9216cd60b613d93e8e/backend/bun.lockb -------------------------------------------------------------------------------- /backend/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | ports: 9 | - "3000:3000" 10 | depends_on: 11 | - redis 12 | environment: 13 | REDIS_HOST: redis 14 | REDIS_PORT: 6379 15 | 16 | redis: 17 | image: redis:alpine 18 | ports: 19 | - "6379:6379" 20 | volumes: 21 | - redis-data:/data 22 | 23 | volumes: 24 | redis-data: 25 | -------------------------------------------------------------------------------- /backend/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cors from "cors"; 3 | import userRouter from "./router/user"; 4 | import eventRouter from "./router/event"; 5 | import bodyParser from "body-parser"; 6 | import { redis, redis2 } from "./services/redisClient"; 7 | 8 | 9 | const app = express(); 10 | await redis.connect().then(() => { 11 | console.log("connected to redis"); 12 | }); 13 | await redis2.connect().then(() => { 14 | console.log("connected to redis2"); 15 | }); 16 | app.use(cors()); 17 | app.use(bodyParser.json()); 18 | app.use("/v1/user", userRouter); 19 | app.use("/v1/event", eventRouter); 20 | 21 | app.listen(3000, () => { 22 | console.log("server is running on 3000"); 23 | }); 24 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "module": "index.ts", 4 | "type": "module", 5 | "devDependencies": { 6 | "@types/bun": "latest" 7 | }, 8 | "peerDependencies": { 9 | "typescript": "^5.0.0" 10 | }, 11 | "dependencies": { 12 | "@paralleldrive/cuid2": "^2.2.2", 13 | "@prisma/client": "5.21.1", 14 | "@types/body-parser": "^1.19.5", 15 | "@types/cors": "^2.8.17", 16 | "@types/express": "^5.0.0", 17 | "body-parser": "^1.20.3", 18 | "cors": "^2.8.5", 19 | "express": "^4.21.1", 20 | "ioredis": "^5.4.1", 21 | "prisma": "^5.21.1", 22 | "redis": "^4.7.0", 23 | "types": "^0.1.1" 24 | } 25 | } -------------------------------------------------------------------------------- /backend/router/event.ts: -------------------------------------------------------------------------------- 1 | import { createId } from "@paralleldrive/cuid2"; 2 | import { Router } from "express"; 3 | import { engineQueue, redis } from "../services/redisClient"; 4 | 5 | const router = Router(); 6 | 7 | router.post("/", async (req, res) => { 8 | const responseId = createId(); 9 | const data = JSON.stringify({ 10 | responseId, 11 | type: "getEvents", 12 | }); 13 | 14 | await redis.subscribe("getEvent", (data) => { 15 | const parseData = JSON.parse(data); 16 | if (parseData.responseId == responseId && parseData.status == "SUCCESS") { 17 | redis.unsubscribe("getEvent"); 18 | return res.json(parseData.events); 19 | } 20 | redis.unsubscribe("getEvent"); 21 | return res.status(401).json({ message: "error fetching events" }); 22 | }); 23 | await engineQueue(data); 24 | }); 25 | 26 | router.post("/initiate", async (req, res) => { 27 | const { userId, eventId, side, price, quantity } = req.body; 28 | if (!userId || !eventId || !side || !price || !quantity) { 29 | res.status(401).json({ message: "invalid information" }); 30 | return; 31 | } 32 | const responseId = createId(); 33 | const data = JSON.stringify({ 34 | userId, 35 | eventId, 36 | responseId, 37 | side, 38 | price, 39 | quantity, 40 | type: "initiateOrder", 41 | }); 42 | 43 | await redis.subscribe("initiateOrder", (data) => { 44 | const parseData = JSON.parse(data); 45 | if (parseData.responseId === responseId && parseData.status == "SUCCESS") { 46 | redis.unsubscribe("initiateOrder"); 47 | return res.json({ message: "order placed successfully" }); 48 | } 49 | redis.unsubscribe("initiateOrder"); 50 | return res.status(401).json({ message: "error placing order" }); 51 | }); 52 | await engineQueue(data); 53 | }); 54 | 55 | router.post("/exit", async (req, res) => { 56 | const { eventId, userId, orderId, side, price, quantity } = req.body; 57 | 58 | const responseId = createId(); 59 | const data = JSON.stringify({ 60 | userId, 61 | orderId, 62 | eventId, 63 | responseId, 64 | side, 65 | price, 66 | quantity, 67 | type: "orderExit", 68 | }); 69 | 70 | await redis.subscribe("orderExit", (data) => { 71 | const parseData = JSON.parse(data); 72 | if (parseData.responseId == responseId && parseData.status == "SUCCESS") { 73 | redis.unsubscribe("orderExit"); 74 | return res.json({ message: "order exited successfully" }); 75 | } 76 | redis.unsubscribe("orderExit"); 77 | return res.status(401).json({ message: "error exiting the order" }); 78 | }); 79 | await engineQueue(data); 80 | }); 81 | 82 | router.post("/getEvent", async (req, res) => { 83 | const { eventId } = req.body; 84 | const responseId = createId(); 85 | const data = JSON.stringify({ 86 | eventId, 87 | responseId, 88 | type: "eventdetails", 89 | }); 90 | 91 | redis.subscribe("eventdetails", (data) => { 92 | const parseData = JSON.parse(data); 93 | if (parseData.responseId == responseId && parseData.status == "SUCCESS") { 94 | redis.unsubscribe("eventdetails"); 95 | return res.json(parseData.event); 96 | } 97 | redis.unsubscribe("eventdetails"); 98 | return res.json({ message: "error fetching event details" }); 99 | }); 100 | await engineQueue(data); 101 | }); 102 | 103 | export default router; 104 | -------------------------------------------------------------------------------- /backend/router/user.ts: -------------------------------------------------------------------------------- 1 | import { createId } from "@paralleldrive/cuid2"; 2 | import { Router } from "express"; 3 | import { engineQueue, redis } from "../services/redisClient"; 4 | 5 | const router = Router(); 6 | 7 | router.post("/signin", async (req, res) => { 8 | const { userId } = req.body; 9 | 10 | const responseId = createId(); 11 | const data = JSON.stringify({ 12 | userId, 13 | responseId, 14 | type: "userLogin", 15 | }); 16 | 17 | await redis.subscribe("userLogin", (data) => { 18 | const parseData = JSON.parse(data); 19 | if (parseData.responseId == responseId && parseData.status == "SUCCESS") { 20 | redis.unsubscribe("userLogin"); 21 | return res.json({ message: "user login successfull" }); 22 | } 23 | redis.unsubscribe("userLogin"); 24 | return res.status(401).json({ message: "user not found" }); 25 | }); 26 | await engineQueue(data); 27 | }); 28 | 29 | router.post("/create", async (req, res) => { 30 | const responseId = createId(); 31 | const userId = createId(); 32 | const data = JSON.stringify({ 33 | responseId, 34 | userId, 35 | type: "userCreation", 36 | }); 37 | 38 | 39 | 40 | await redis.subscribe("userCreation", (data) => { 41 | const parseData = JSON.parse(data); 42 | if (parseData.responseId == responseId) { 43 | redis.unsubscribe("userCreation"); 44 | return res.json({ 45 | message: `"user added successfully with id:"${userId}`, 46 | }); 47 | } 48 | redis.unsubscribe("userCreation"); 49 | return res.status(401).json({ message: "user failed to create" }); 50 | }); 51 | await engineQueue(data); 52 | }); 53 | 54 | router.post("/recharge", async (req, res) => { 55 | const { userId, amount } = req.body; 56 | const responseId = createId(); 57 | const data = JSON.stringify({ 58 | userId, 59 | amount, 60 | responseId, 61 | type: "userRecharge", 62 | }); 63 | 64 | await redis.subscribe("userRecharge", (data) => { 65 | const parseData = JSON.parse(data); 66 | if (parseData.responseId == responseId && parseData.status === "SUCCESS") { 67 | redis.unsubscribe("userRecharge"); 68 | return res.json(`"Total available balance :"${parseData.balance}`); 69 | } 70 | redis.unsubscribe("userRecharge"); 71 | return res.status(401).json({ message: "user recharge failed" }); 72 | }); 73 | await engineQueue(data); 74 | }); 75 | 76 | router.post("/balance", async (req, res) => { 77 | const { userId } = req.body; 78 | const responseId = createId(); 79 | 80 | const data = JSON.stringify({ 81 | userId, 82 | responseId, 83 | type: "userBalance", 84 | }); 85 | 86 | await redis.subscribe("userBalance", (data) => { 87 | const parseData = JSON.parse(data); 88 | if (parseData.responseId == responseId && parseData.status === "SUCCESS") { 89 | redis.unsubscribe("userBalance"); 90 | 91 | return res.json({ balance: parseData.balance }); 92 | } 93 | redis.unsubscribe("userBalance"); 94 | return res.json("error fetching the balance"); 95 | }); 96 | await engineQueue(data); 97 | }); 98 | 99 | export default router; 100 | -------------------------------------------------------------------------------- /backend/services/redisClient.ts: -------------------------------------------------------------------------------- 1 | import Redis from "ioredis"; 2 | import { createClient } from "redis"; 3 | 4 | export const redis = createClient({ 5 | socket: { 6 | host: "redis", 7 | port: 6379, 8 | }, 9 | }); 10 | 11 | export const redis2 = createClient({ 12 | socket: { 13 | host: "redis2", 14 | port: 6379, 15 | }, 16 | }); 17 | 18 | redis.on("error", (error) => { 19 | console.log(error); 20 | }); 21 | 22 | export async function engineQueue(data: any) { 23 | redis2.lPush("engineQueue", data); 24 | } 25 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/utils/cashfree.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryanpachori/opinion_trading/d31cf86d2394c47d739e1a9216cd60b613d93e8e/backend/utils/cashfree.ts -------------------------------------------------------------------------------- /backend/utils/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | import dotenv from "dotenv" 3 | dotenv.config() 4 | 5 | const prismaClientSingleton = () => { 6 | return new PrismaClient() 7 | } 8 | 9 | declare global { 10 | var prismaGlobal: undefined | ReturnType 11 | } 12 | 13 | const prisma: ReturnType = globalThis.prismaGlobal ?? prismaClientSingleton() 14 | 15 | export default prisma 16 | 17 | if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma -------------------------------------------------------------------------------- /client/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | node_modules 4 | npm-debug.log 5 | README.md 6 | .next 7 | .git -------------------------------------------------------------------------------- /client/.env.example: -------------------------------------------------------------------------------- 1 | 2 | NEXTAUTH_URL="" 3 | NEXTAUTH_SECRET="" 4 | TWILIO_ACCOUNT_SID="" 5 | TWILIO_AUTH_TOKEN="" 6 | TWILIO_NUMBER="" -------------------------------------------------------------------------------- /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /client/.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 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM node:20 3 | 4 | 5 | WORKDIR /app 6 | COPY package.json bun.lockb ./ 7 | RUN npm install -g bun 8 | RUN bun install 9 | 10 | 11 | COPY . . 12 | RUN bunx prisma generate 13 | 14 | EXPOSE 3001 15 | 16 | CMD [ "bun", "run", "dev" ] 17 | -------------------------------------------------------------------------------- /client/actions/balance/balance.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import axios from "axios"; 3 | export async function getBalance(userId: string) { 4 | const response = await axios.post( 5 | `http://backend-service:3000/v1/user/balance`, 6 | { 7 | userId, 8 | } 9 | ); 10 | 11 | if (response.status === 200) { 12 | const data = response.data; 13 | console.log("data", data); 14 | const balance = data.balance; 15 | console.log("balance", balance); 16 | return balance; 17 | } else { 18 | throw new Error("Balance not found"); 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/actions/balance/order.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { Cashfree } from "cashfree-pg"; 4 | 5 | Cashfree.XClientId = process.env.CASHFREE_CLIENT_API_KEY as string; 6 | Cashfree.XClientSecret = process.env.CASHFREE_CLIENT_PASSWORD as string; 7 | Cashfree.XEnvironment = Cashfree.Environment.PRODUCTION; 8 | 9 | export async function createOrder() { 10 | console.log("request here"); 11 | const request = { 12 | order_amount: 1, 13 | order_currency: "INR", 14 | customer_details: { 15 | customer_id: "node_sdk_test", 16 | customer_name: "", 17 | customer_email: "example@gmail.com", 18 | customer_phone: "9999999999", 19 | }, 20 | order_meta: { 21 | return_url: 22 | "https://test.cashfree.com/pgappsdemos/return.php?order_id=order_123", 23 | }, 24 | order_note: "", 25 | }; 26 | console.log("hrere too"); 27 | 28 | Cashfree.PGCreateOrder("2023-08-01", request) 29 | .then((response) => { 30 | const a = response.data; 31 | console.log(a); 32 | }) 33 | .catch((error) => { 34 | console.error("Error setting up order request:", error.response.data); 35 | }); 36 | 37 | console.log("hrere too jdskfjd"); 38 | } 39 | -------------------------------------------------------------------------------- /client/actions/events/event.ts: -------------------------------------------------------------------------------- 1 | 2 | "use server" 3 | import axios from "axios"; 4 | 5 | export async function getEvents() { 6 | const response = await axios.post(`http://backend-service:3000/v1/event`); 7 | 8 | if (response.status == 200) { 9 | console.log("responseData :",response.data) 10 | 11 | return response.data 12 | } 13 | throw new Error("error fetching events"); 14 | } 15 | -------------------------------------------------------------------------------- /client/actions/portfolio/portfolio.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { PrismaClient } from "@prisma/client"; 3 | 4 | const prisma = new PrismaClient(); 5 | export async function getOrders(userId: string) { 6 | const portfolio = await prisma.order.findMany({ 7 | where: { 8 | userId: userId, 9 | }, 10 | }); 11 | 12 | const eventIds = Array.from(new Set(portfolio.map((trade) => trade.eventId))); 13 | 14 | const events = await prisma.event.findMany({ 15 | where: { id: { in: eventIds } }, 16 | }); 17 | console.log("events", events); 18 | 19 | const portfolioWithTitles = portfolio.map((trade) => ({ 20 | ...trade, 21 | title: 22 | events.find((event) => event.id === trade.eventId)?.title || 23 | "Unknown Event", 24 | })); 25 | return portfolioWithTitles 26 | 27 | } 28 | -------------------------------------------------------------------------------- /client/actions/recharge/recharge.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | import axios from "axios"; 3 | 4 | export async function recharge(userId: string, depositAmount: number) { 5 | const response = await axios.post( 6 | ` http://backend-service:3000/v1/user/recharge`, 7 | { 8 | userId, 9 | amount: depositAmount, 10 | } 11 | ); 12 | if (response.status === 200) { 13 | return { success: true }; 14 | } 15 | throw new Error("error to recharge"); 16 | return 17 | } 18 | -------------------------------------------------------------------------------- /client/app/(lobby)/auth/signin/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Login } from "../../../../components/landing/Singin"; 3 | 4 | const Page = () => { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | }; 11 | 12 | export default Page; 13 | -------------------------------------------------------------------------------- /client/app/(lobby)/event/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import OrderBook from "@/components/landing/Orderbook"; 4 | import { useParams } from "next/navigation"; 5 | 6 | export default function Page() { 7 | const { id } = useParams(); 8 | console.log("id:",id); 9 | const eventId = Array.isArray(id) ? id[id.length - 1] : id; 10 | console.log(eventId) 11 | if (!eventId) { 12 | return
Error: Event ID not found
; 13 | } 14 | return ( 15 |
16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /client/app/(lobby)/event/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from "react"; 3 | import EventList from "../../components/EventList"; 4 | 5 | import axios from "axios"; 6 | 7 | const page = async () => { 8 | const response = await axios.post(`http://backend-service:3000/v1/event`); 9 | 10 | return ( 11 |
12 | 13 |
14 | ); 15 | }; 16 | 17 | export default page; 18 | -------------------------------------------------------------------------------- /client/app/(lobby)/event/recharge/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import DepositForm from "../../../../components/landing/DepositForm"; 4 | 5 | 6 | const Page = () => { 7 | 8 | 9 | return ( 10 |
11 | 12 | 13 |
14 | ); 15 | }; 16 | 17 | export default Page; 18 | -------------------------------------------------------------------------------- /client/app/(lobby)/portfolio/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useCallback, useEffect, useState } from "react"; 3 | 4 | import Portfolio from "../../../components/landing/Portfolio"; 5 | 6 | import toast, { Toaster } from "react-hot-toast"; 7 | import { getOrders } from "@/actions/portfolio/portfolio"; 8 | import axios from "axios"; 9 | import { useSession } from "next-auth/react"; 10 | import { redirect } from "next/navigation"; 11 | 12 | export interface Trade { 13 | id: string; 14 | userId: string; 15 | price: number; 16 | Quantity: number; 17 | Side: "YES" | "NO"; 18 | type: "BUY" | "SELL"; 19 | status: "EXECUTED" | "LIVE"; 20 | eventId: string; 21 | title?: string; 22 | } 23 | 24 | const Page = () => { 25 | const [loading, setLoading] = useState(true); 26 | const [portfolioData, setPortfolioData] = useState([]); 27 | const { data : session , status } = useSession(); 28 | 29 | if (status == "unauthenticated") { 30 | redirect("/auth/signin"); 31 | } 32 | const userId =session?.user.id; 33 | 34 | useEffect(() => { 35 | 36 | if (userId) { 37 | getPortfolioDetails(userId); 38 | } 39 | }, [userId]); 40 | 41 | const getPortfolioDetails = useCallback(async (userId: string) => { 42 | setLoading(true); 43 | try { 44 | const portfolio = await getOrders(userId); 45 | console.log("portfolio", portfolio); 46 | 47 | setPortfolioData(portfolio); 48 | } catch (e) { 49 | console.log("Error fetching portfolio", e); 50 | } finally { 51 | setLoading(false); 52 | } 53 | }, []); 54 | 55 | if (loading) { 56 | return
Loading...
; 57 | } 58 | if (!portfolioData) { 59 | return
No portfolio found.
; 60 | } 61 | async function handleExit(trade: Trade) { 62 | const { id, price, Quantity, eventId, Side, userId } = trade; 63 | const response = await axios.post( 64 | `http://localhost:3000/v1/event/exit`, 65 | { 66 | userId, 67 | eventId, 68 | orderId: id, 69 | side: Side, 70 | price, 71 | quantity: Quantity, 72 | } 73 | ); 74 | if (response.status === 200) { 75 | toast.success("Order processed succesfully"); 76 | setTimeout(() => { 77 | window.location.reload(); 78 | }, 1000); 79 | } else { 80 | toast.error("Error processing order"); 81 | } 82 | } 83 | 84 | return ( 85 |
86 | ({ 90 | id: trade.id, 91 | userId: trade.userId, 92 | price: trade.price, 93 | Quantity: trade.Quantity, 94 | Side: trade.Side, 95 | type: trade.type, 96 | status: trade.status, 97 | eventId: trade.eventId, 98 | title: trade.title, 99 | }))} 100 | /> 101 | 102 |
103 | ); 104 | }; 105 | 106 | export default Page; 107 | -------------------------------------------------------------------------------- /client/app/_provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React from 'react'; 3 | import { SessionProvider } from 'next-auth/react'; 4 | 5 | export const SessionProviders = ({ children }: { children: React.ReactNode }) => { 6 | return ( 7 | 8 | {children} 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /client/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { authOptions } from "@/lib/auth" 2 | import NextAuth from "next-auth" 3 | 4 | const handler = NextAuth(authOptions) 5 | 6 | export { handler as GET, handler as POST } -------------------------------------------------------------------------------- /client/app/components/EventList.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | "use client"; 3 | import React from "react"; 4 | import { Card, CardContent } from "@/components/ui/card"; 5 | import { Button } from "@/components/ui/button"; 6 | import Link from "next/link"; 7 | 8 | interface Event { 9 | id: string; 10 | title: string; 11 | description: string; 12 | } 13 | 14 | interface EventListProps { 15 | events: Record; 16 | } 17 | 18 | const getRandomAvatar = (seed: string) => 19 | `https://api.dicebear.com/9.x/identicon/svg?seed=${seed}`; 20 | 21 | const EventCard = ({ event }: { event: Event }) => { 22 | const avatarUrl = getRandomAvatar(event.id); 23 | 24 | return ( 25 | 26 | 27 |
28 | Event Avatar 33 |
34 | 35 |

{event.title}

36 | 37 |
38 |
39 |

40 | {event.description.slice(0, 100).toLowerCase()} 41 | read more.. 42 |

43 |
44 | 45 | 48 |
49 |
50 |
51 | ); 52 | }; 53 | 54 | export default function EventList({ events }: EventListProps) { 55 | const eventsArray = Object.entries(events).map(([id, event]) => ({ 56 | id, 57 | title: event.title, 58 | description: event.description, 59 | })); 60 | 61 | if (eventsArray.length === 0) { 62 | return ( 63 |
64 |

No events available

65 |
66 | ); 67 | } 68 | 69 | return ( 70 |
71 |

72 | All Events 73 |

74 |
75 | {eventsArray.map((event) => ( 76 | 77 | ))} 78 |
79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /client/app/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Hero} from '../../components/landing/Hero' 3 | import TakesCareWrapper from "../../components/landing/TakesCare" 4 | import FAQS from "../../components/landing/FAQs" 5 | 6 | export const HomeComponent = () => { 7 | return ( 8 |
9 | 10 |
11 | 12 |
13 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /client/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryanpachori/opinion_trading/d31cf86d2394c47d739e1a9216cd60b613d93e8e/client/app/favicon.ico -------------------------------------------------------------------------------- /client/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryanpachori/opinion_trading/d31cf86d2394c47d739e1a9216cd60b613d93e8e/client/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /client/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryanpachori/opinion_trading/d31cf86d2394c47d739e1a9216cd60b613d93e8e/client/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /client/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer utilities { 10 | .text-balance { 11 | text-wrap: balance; 12 | } 13 | } 14 | 15 | @layer base { 16 | :root { 17 | --background: 0 0% 100%; 18 | --foreground: 240 10% 3.9%; 19 | --card: 0 0% 100%; 20 | --card-foreground: 240 10% 3.9%; 21 | --popover: 0 0% 100%; 22 | --popover-foreground: 240 10% 3.9%; 23 | --primary: 240 5.9% 10%; 24 | --primary-foreground: 0 0% 98%; 25 | --secondary: 240 4.8% 95.9%; 26 | --secondary-foreground: 240 5.9% 10%; 27 | --muted: 240 4.8% 95.9%; 28 | --muted-foreground: 240 3.8% 46.1%; 29 | --accent: 240 4.8% 95.9%; 30 | --accent-foreground: 240 5.9% 10%; 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 0 0% 98%; 33 | --border: 240 5.9% 90%; 34 | --input: 240 5.9% 90%; 35 | --ring: 240 10% 3.9%; 36 | --chart-1: 12 76% 61%; 37 | --chart-2: 173 58% 39%; 38 | --chart-3: 197 37% 24%; 39 | --chart-4: 43 74% 66%; 40 | --chart-5: 27 87% 67%; 41 | --radius: 0.5rem; 42 | } 43 | .dark { 44 | --background: 231 54% 5%; /* Updated to #05071A */ 45 | --foreground: 0 0% 98%; 46 | --card: 231 54% 5%; /* Updated to #05071A */ 47 | --card-foreground: 0 0% 98%; 48 | --popover: 240 10% 3.9%; 49 | --popover-foreground: 0 0% 98%; 50 | --primary: 0 0% 98%; 51 | --primary-foreground: 240 5.9% 10%; 52 | --secondary: 240 3.7% 15.9%; 53 | --secondary-foreground: 0 0% 98%; 54 | --muted: 240 3.7% 15.9%; 55 | --muted-foreground: 240 5% 64.9%; 56 | --accent: 240 3.7% 15.9%; 57 | --accent-foreground: 0 0% 98%; 58 | --destructive: 0 62.8% 30.6%; 59 | --destructive-foreground: 0 0% 98%; 60 | --border: 240 3.7% 15.9%; 61 | --input: 240 3.7% 15.9%; 62 | --ring: 240 4.9% 83.9%; 63 | --chart-1: 220 70% 50%; 64 | --chart-2: 160 60% 45%; 65 | --chart-3: 30 80% 55%; 66 | --chart-4: 280 65% 60%; 67 | --chart-5: 340 75% 55%; 68 | } 69 | 70 | } 71 | 72 | @layer base { 73 | * { 74 | @apply border-border; 75 | } 76 | body { 77 | @apply bg-background text-foreground; 78 | } 79 | } 80 | 81 | .gradient-text { 82 | background: linear-gradient( 83 | to right, 84 | #a855f7, 85 | #3178c6, 86 | #b51d1d, 87 | #3178c6, 88 | #a855f7, 89 | #3178c6, 90 | #b51d1d 91 | ); 92 | background-size: 300% 300%; 93 | background-clip: text; 94 | } 95 | 96 | .gradient-border-left { 97 | border-radius: 2rem; 98 | border: 2px solid; 99 | border-image: linear-gradient( 100 | to right, 101 | white, 102 | rgb(37, 36, 38), 103 | rgb(233, 219, 237) 104 | ); 105 | border-image-slice: 1; 106 | border-image-width: 1 0 1 1; 107 | } 108 | .gradient-border-right { 109 | border-radius: 2rem; 110 | border: 2px solid; 111 | border-image: linear-gradient( 112 | to right, 113 | white, 114 | rgb(37, 36, 38), 115 | rgb(233, 219, 237) 116 | ); 117 | border-image-slice: 1; 118 | border-image-width: 1 1 1 0; 119 | } -------------------------------------------------------------------------------- /client/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | import { ThemeProvider } from "@/components/theme-provider"; 5 | import { SessionProviders } from "./_provider"; 6 | import Appbar from "@/components/landing/Appbar"; 7 | 8 | const geistSans = localFont({ 9 | src: "./fonts/GeistVF.woff", 10 | variable: "--font-geist-sans", 11 | weight: "100 900", 12 | }); 13 | const geistMono = localFont({ 14 | src: "./fonts/GeistMonoVF.woff", 15 | variable: "--font-geist-mono", 16 | weight: "100 900", 17 | }); 18 | 19 | export const metadata: Metadata = { 20 | title: "OpiniX", 21 | description: "An Opinion Trading Platform", 22 | }; 23 | 24 | export default function RootLayout({ 25 | children, 26 | }: Readonly<{ 27 | children: React.ReactNode; 28 | }>) { 29 | return ( 30 | 31 | 34 | 40 | 41 | 42 | {children} 43 | 44 | 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /client/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import {HomeComponent} from "./components/Home" 3 | 4 | export default function Page() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /client/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryanpachori/opinion_trading/d31cf86d2394c47d739e1a9216cd60b613d93e8e/client/bun.lockb -------------------------------------------------------------------------------- /client/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /client/components/landing/Appbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | import React, { useEffect, useState } from "react"; 4 | import { NavigationMenu } from "./Navmenu"; 5 | import ProfileHeader from "./ProfileHeader"; 6 | import { Wallet } from "lucide-react"; 7 | import { getBalance } from "@/actions/balance/balance"; 8 | import { useSession } from "next-auth/react"; 9 | //import { redirect } from "next/navigation"; 10 | 11 | interface navMenutItemType { 12 | title: string; 13 | link: string; 14 | } 15 | 16 | export default function Appbar() { 17 | const [balance, setBalance] = useState(0); 18 | const { data: session, status } = useSession(); 19 | 20 | 21 | if (status === "unauthenticated") { 22 | //redirect("/auth/signin"); 23 | } 24 | 25 | const userId = session?.user?.id; 26 | 27 | 28 | useEffect(() => { 29 | async function loadBal() { 30 | if (userId) { 31 | const bal = await getBalance(userId); 32 | console.log("Balance:", bal); 33 | setBalance(bal); 34 | } 35 | } 36 | loadBal(); 37 | }, [userId]); 38 | 39 | const navMenuItems: Array = [ 40 | { title: "Events", link: "/event" }, 41 | { title: "Portfolio", link: "/portfolio" }, 42 | { title: "Recharge", link: "/event/recharge" }, 43 | ]; 44 | 45 | return ( 46 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /client/components/landing/DepositForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState, useEffect } from "react"; 3 | import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; 4 | import { Input } from "../ui/input"; 5 | import { Button } from "../ui/button"; 6 | import { Label } from "../ui/label"; 7 | import { Toaster, toast } from "react-hot-toast"; 8 | // import { recharge } from "@/actions/recharge/recharge"; 9 | // import { createOrder } from "@/actions/balance/order"; 10 | import axios from "axios"; 11 | import dotenv from "dotenv"; 12 | import { useSession } from "next-auth/react"; 13 | import { redirect } from "next/navigation"; 14 | dotenv.config(); 15 | const DepositForm = () => { 16 | const { data: session, status } = useSession(); 17 | 18 | 19 | if (status === "unauthenticated") { 20 | redirect("/auth/signin"); 21 | } 22 | 23 | 24 | const [depositAmount, setDepositAmount] = useState(0); 25 | const [summary, setSummary] = useState({ 26 | rechargeAmount: 0, 27 | gst: 0, 28 | depositBalCredit: 0, 29 | promotionalBalCredit: 0, 30 | netBalance: 0, 31 | }); 32 | 33 | useEffect(() => { 34 | const rechargeAmount = depositAmount; 35 | const gst = -(depositAmount * 0.18).toFixed(2); 36 | const depositBalCredit = Number((depositAmount - Math.abs(gst)).toFixed(2)); 37 | const promotionalBalCredit = Math.abs(gst); 38 | const netBalance = depositAmount; 39 | 40 | setSummary({ 41 | rechargeAmount, 42 | gst, 43 | depositBalCredit, 44 | promotionalBalCredit, 45 | netBalance, 46 | }); 47 | }, [depositAmount]); 48 | 49 | const handleQuickAdd = (amount: number) => { 50 | setDepositAmount((prev) => prev + amount); 51 | }; 52 | 53 | async function handleRechageClick() { 54 | const userId = session?.user?.id; 55 | // const orderId = await createOrder(depositAmount , userId) 56 | 57 | //console.log(orderId); 58 | if (depositAmount <= 0) { 59 | toast.error("amount should be greater than zero"); 60 | return; 61 | } 62 | 63 | const isRechargeDone = await axios.post( 64 | ` http://localhost:3000/v1/user/recharge`, 65 | { 66 | userId, 67 | amount: depositAmount, 68 | } 69 | ); 70 | if (isRechargeDone.status == 200) { 71 | toast.success("Recharge Success"); 72 | setTimeout(() => { 73 | window.location.reload(); 74 | }, 1000); 75 | } else { 76 | toast.error("Error While Recharing"); 77 | } 78 | } 79 | 80 | return ( 81 |
82 |
83 | 84 |

Deposit

85 | 86 | 87 | setDepositAmount(Number(e.target.value))} 92 | className="mb-4" 93 | /> 94 |
95 | 98 | 101 | 104 |
105 | 112 |
113 |
114 | 115 | 116 | SUMMARY 117 | 118 | 119 |
120 |
121 | Recharge amount 122 | ₹{summary.rechargeAmount.toFixed(2)} 123 |
124 |
125 | GST applicable 126 | ₹{summary.gst} 127 |
128 |
129 | Deposit bal. credit 130 | ₹{summary.depositBalCredit} 131 |
132 |
133 | Promotional bal. credit 134 | 135 | + ₹{summary.promotionalBalCredit.toFixed(2)} 136 | 137 |
138 |
🎉 Recharge Cashback
139 |
140 | Net Balance 141 | ₹{summary.netBalance.toFixed(2)} 142 |
143 |
144 |
145 |
146 |
147 | 148 |
149 | ); 150 | }; 151 | 152 | export default DepositForm; 153 | -------------------------------------------------------------------------------- /client/components/landing/FAQs.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Accordion, 4 | AccordionContent, 5 | AccordionItem, 6 | AccordionTrigger, 7 | } from "../ui/accordion"; 8 | 9 | const FAQS = () => { 10 | return ( 11 |
12 | 17 |
18 |

19 | F 20 | A 21 | Qs 22 |

23 |
24 | 25 | 29 | 30 | How does OpiniX ensure security? 31 | 32 | 33 | Centre to constitute the 8th Pay Commission? 34 | 35 | 36 | 40 | 41 | is it safe 42 | 43 | 44 | yes 100% 45 | 46 | 47 | 51 | 52 | Will money withdrawals easy? 53 | 54 | 55 | yes it is easy, just one click. 56 | 57 | 58 | 62 | 63 | is there any mobile app? 64 | 65 | 66 | Yes,there will mobile app soon, in v2. 67 | 68 | 69 | 73 | 74 | How can I get started with OpiniX? 75 | 76 | 77 | just register and start trading. 78 | 79 | 80 | 81 |
82 | ); 83 | }; 84 | 85 | export default FAQS; -------------------------------------------------------------------------------- /client/components/landing/Hero.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import Link from "next/link"; 3 | import ReactWrapBalancer from "react-wrap-balancer"; 4 | //import { useSession } from "next-auth/react"; 5 | import { Button } from "../ui/button"; 6 | 7 | export function Hero() { 8 | 9 | return ( 10 |
11 |
12 |
13 | {/* Hero content */} 14 |
15 |
16 |
17 | 22 | 23 | Mobile app available. Download now!{" "} 24 | 25 | > 26 | 27 | 28 | 29 |
30 |
31 |

35 | 36 | Trade your opinion{" "}using OpiniX 37 | 38 |

39 |

44 | buy and sell shares on various events, and profit from your insights. 45 |

46 |
51 | 52 |
53 |
54 |
55 |
56 |
57 | ); 58 | } -------------------------------------------------------------------------------- /client/components/landing/Navmenu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import Link from "next/link"; 3 | import React from 'react'; 4 | type NavigationMenuProps = { 5 | title: string; 6 | link: string; 7 | } 8 | export const NavigationMenu = ({title, link}:NavigationMenuProps) => { 9 | return ( 10 | 14 | {title} 15 | 16 | ) 17 | } -------------------------------------------------------------------------------- /client/components/landing/Portfolio.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState } from "react"; 4 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 5 | import { Button } from "@/components/ui/button"; 6 | import { ArrowUpIcon, ArrowDownIcon } from "lucide-react"; 7 | import { Trade } from "@/app/(lobby)/portfolio/page"; 8 | 9 | interface PortfolioProps { 10 | currentReturns: number; 11 | trades: Trade[]; 12 | onExit: (trade: Trade) => void; 13 | } 14 | 15 | export default function Portfolio({ 16 | currentReturns, 17 | trades, 18 | onExit, 19 | }: PortfolioProps) { 20 | const [activeSection, setActiveSection] = useState<"trades" | "pending">( 21 | "trades" 22 | ); 23 | 24 | const activeTrades = trades.filter( 25 | (trade) => trade.status === "LIVE" && trade.type === "BUY" 26 | ); 27 | const pastTrades = trades.filter( 28 | (trade) => trade.status === "EXECUTED" && trade.type === "SELL" 29 | ); 30 | const pendingBuy = trades.filter( 31 | (trade) => trade.status === "LIVE" && trade.type === "SELL" 32 | ); 33 | const pendingSell = trades.filter( 34 | (trade) => trade.status === "EXECUTED" && trade.type === "BUY" 35 | ); 36 | 37 | return ( 38 |
39 | 40 | 41 | 42 | Your Portfolio 43 | 44 | 45 | 46 |
47 |

Current Returns

48 |

= 0 ? "text-green-500" : "text-red-500" 51 | }`} 52 | > 53 | {currentReturns >= 0 ? "+" : "-"}₹ 54 | {Math.abs(currentReturns).toFixed(2)} 55 | {currentReturns >= 0 ? ( 56 | 57 | ) : ( 58 | 59 | )} 60 |

61 |
62 | 63 | {/* Toggle Buttons for Sections */} 64 |
65 | 71 | 77 |
78 | 79 | {/* Active and Past Trades Section */} 80 | {activeSection === "trades" && ( 81 |
82 |

83 | Active & Past Trades 84 |

85 |
86 | {activeTrades.length > 0 ? ( 87 | activeTrades.map((trade) => ( 88 | 92 | 93 |
94 |
95 |

96 | {trade.title} 97 |

98 |

99 | Price: ₹{trade.price.toFixed(2)} | Quantity:{" "} 100 | {trade.Quantity} 101 |

102 |
103 |
104 | 111 | {trade.Side.toUpperCase()} 112 | 113 | 123 |
124 |
125 |
126 |
127 | )) 128 | ) : ( 129 |

No active trades.

130 | )} 131 | {pastTrades.length > 0 ? ( 132 | pastTrades.map((trade) => ( 133 | 137 | 138 |
139 |
140 |

141 | {trade.title} 142 |

143 |

144 | Price: ₹{trade.price.toFixed(2)} | Quantity:{" "} 145 | {trade.Quantity} 146 |

147 |

= 0 150 | ? "text-green-500" 151 | : "text-red-500" 152 | }`} 153 | > 154 | Gain/Loss:{" "} 155 | {trade.price !== null 156 | ? trade.price >= 0 157 | ? "+" 158 | : "-" 159 | : ""} 160 | ₹{Math.abs(trade.price || 0).toFixed(2)} 161 |

162 |
163 |
164 | 171 | {trade.Side.toUpperCase()} 172 | 173 |
174 |
175 |
176 |
177 | )) 178 | ) : ( 179 |

No past trades.

180 | )} 181 |
182 |
183 | )} 184 | 185 | {/* Pending Orders Section */} 186 | {activeSection === "pending" && ( 187 |
188 |

Pending Orders

189 |
190 | {pendingBuy.length > 0 && 191 | pendingBuy.map((trade) => ( 192 | 196 | 197 |
198 |
199 |

200 | {trade.title} 201 |

202 |

203 | Price: ₹{trade.price.toFixed(2)} | Quantity:{" "} 204 | {trade.Quantity} 205 |

206 |
207 |
208 | 215 | {trade.Side.toUpperCase()} 216 | 217 | 224 | {trade.type.toUpperCase() == "BUY" ? "SELL" : "BUY"} 225 | 226 |
227 |
228 |
229 |
230 | ))} 231 | {pendingSell.length > 0 ? ( 232 | pendingSell.map((trade) => ( 233 | 237 | 238 |
239 |
240 |

241 | {trade.title} 242 |

243 | 244 |

245 | Price: ₹{trade.price.toFixed(2)} | Quantity:{" "} 246 | {trade.Quantity} 247 |

248 |
249 |
250 | 257 | {trade.Side.toUpperCase()} 258 | 259 | 266 | {trade.type.toUpperCase() == "BUY" ? "SELL" : "BUY"} 267 | 268 | 269 |
270 |
271 |
272 |
273 | )) 274 | ) : ( 275 |

276 | No pending sell orders. 277 |

278 | )} 279 |
280 |
281 | )} 282 |
283 |
284 |
285 | ); 286 | } 287 | -------------------------------------------------------------------------------- /client/components/landing/ProfileHeader.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { Button } from "../ui/button"; 4 | import Link from "next/link"; 5 | import { useSession, signOut } from "next-auth/react"; 6 | 7 | const ProfileHeader = () => { 8 | const { data } = useSession(); 9 | 10 | return ( 11 |
12 | 13 | 16 | 17 | {data?.user && ( 18 | 24 | )} 25 |
26 | ); 27 | }; 28 | 29 | export default ProfileHeader; 30 | -------------------------------------------------------------------------------- /client/components/landing/Singin.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import Image from "next/image" 3 | import { Button } from "@/components/ui/button" 4 | import login from "@/public/login.png" 5 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 6 | import { signIn, signOut, useSession } from "next-auth/react"; 7 | import { LoaderCircle } from "lucide-react"; 8 | import { toast, Toaster } from "react-hot-toast"; 9 | 10 | export function Login() { 11 | const { data: session, status } = useSession(); 12 | const isLoading = status === "loading"; 13 | 14 | async function handleSignIn() { 15 | const result = await signIn("google", { redirect: false }); 16 | 17 | if (result?.error) { 18 | toast.error("Error signing in, please try again."); 19 | } else { 20 | toast.success("Successfully signed in!"); 21 | } 22 | } 23 | 24 | async function handleSignOut() { 25 | await signOut({ redirect: false }); 26 | toast.success("Successfully signed out!"); 27 | } 28 | 29 | return ( 30 |
31 |
32 | 33 | 34 | {session ? "Welcome back!" : "Sign in with Google"} 35 | 36 | 37 | {session ? ( 38 |
39 |

Logged in as {session.user.email}

40 | 43 |
44 | ) : ( 45 |
46 |

Sign in to continue

47 | 50 |
51 | )} 52 |

53 | By continuing, you accept that you are 18+ years of age & agree to the{' '} 54 | 55 | Terms and Conditions 56 | 57 |

58 |
59 |
60 |
61 |
62 |
63 | Image 70 |
71 |
72 | 73 |
74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /client/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ThemeProvider as NextThemesProvider } from "next-themes" 5 | import { type ThemeProviderProps } from "next-themes/dist/types" 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children} 9 | } 10 | -------------------------------------------------------------------------------- /client/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDownIcon } from "@radix-ui/react-icons" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Accordion = AccordionPrimitive.Root 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | AccordionItem.displayName = "AccordionItem" 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )) 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |
{children}
53 |
54 | )) 55 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 56 | 57 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 58 | -------------------------------------------------------------------------------- /client/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /client/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /client/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /client/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |

53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |

61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /client/components/ui/chart.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as RechartsPrimitive from "recharts" 5 | import { cn } from "@/lib/utils" 6 | 7 | // Format: { THEME_NAME: CSS_SELECTOR } 8 | const THEMES = { light: "", dark: ".dark" } as const 9 | 10 | export type ChartConfig = { 11 | [k in string]: { 12 | label?: React.ReactNode 13 | icon?: React.ComponentType 14 | } & ( 15 | | { color?: string; theme?: never } 16 | | { color?: never; theme: Record } 17 | ) 18 | } 19 | 20 | type ChartContextProps = { 21 | config: ChartConfig 22 | } 23 | 24 | const ChartContext = React.createContext(null) 25 | 26 | function useChart() { 27 | const context = React.useContext(ChartContext) 28 | 29 | if (!context) { 30 | throw new Error("useChart must be used within a ") 31 | } 32 | 33 | return context 34 | } 35 | 36 | const ChartContainer = React.forwardRef< 37 | HTMLDivElement, 38 | React.ComponentProps<"div"> & { 39 | config: ChartConfig 40 | children: React.ComponentProps< 41 | typeof RechartsPrimitive.ResponsiveContainer 42 | >["children"] 43 | } 44 | >(({ id, className, children, config, ...props }, ref) => { 45 | const uniqueId = React.useId() 46 | const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` 47 | 48 | return ( 49 | 50 |
59 | 60 | 61 | {children} 62 | 63 |
64 |
65 | ) 66 | }) 67 | ChartContainer.displayName = "Chart" 68 | 69 | const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { 70 | const colorConfig = Object.entries(config).filter( 71 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 72 | ([_, config]) => config.theme || config.color 73 | ) 74 | 75 | if (!colorConfig.length) { 76 | return null 77 | } 78 | 79 | return ( 80 |