├── .eslintrc.js
├── .github
└── workflows
│ ├── build.yml
│ └── deploy.yml
├── .gitignore
├── .npmrc
├── README.md
├── apps
├── bank-webhook
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── tsconfig.json
├── merchant-app
│ ├── .eslintrc.js
│ ├── README.md
│ ├── app
│ │ ├── api
│ │ │ └── auth
│ │ │ │ └── [...nextauth]
│ │ │ │ └── route.ts
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── page.module.css
│ │ └── page.tsx
│ ├── lib
│ │ └── auth.ts
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── package.json
│ ├── postcss.config.js
│ ├── provider.tsx
│ ├── public
│ │ ├── circles.svg
│ │ ├── next.svg
│ │ ├── turborepo.svg
│ │ └── vercel.svg
│ ├── tailwind.config.js
│ └── tsconfig.json
└── user-app
│ ├── .env.example
│ ├── .eslintrc.js
│ ├── README.md
│ ├── app
│ ├── (dashboard)
│ │ ├── dashboard
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── p2p
│ │ │ └── page.tsx
│ │ ├── transactions
│ │ │ └── page.tsx
│ │ └── transfer
│ │ │ └── page.tsx
│ ├── api
│ │ ├── auth
│ │ │ └── [...nextauth]
│ │ │ │ └── route.ts
│ │ └── user
│ │ │ └── route.ts
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ ├── lib
│ │ ├── actions
│ │ │ ├── createOnRamptxn.ts
│ │ │ └── p2pTransfer.ts
│ │ └── auth.ts
│ ├── page.module.css
│ └── page.tsx
│ ├── components
│ ├── AddMoneyCard.tsx
│ ├── AppbarClient.tsx
│ ├── BalanceCard.tsx
│ ├── OnRampTransactions.tsx
│ ├── SendCard.tsx
│ └── SidebarItem.tsx
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── package.json
│ ├── postcss.config.js
│ ├── provider.tsx
│ ├── public
│ ├── circles.svg
│ ├── next.svg
│ ├── turborepo.svg
│ └── vercel.svg
│ ├── tailwind.config.js
│ └── tsconfig.json
├── docker
└── Dockerfile.user
├── package-lock.json
├── package.json
├── packages
├── db
│ ├── .env.example
│ ├── .gitignore
│ ├── index.ts
│ ├── package.json
│ ├── prisma
│ │ ├── migrations
│ │ │ ├── 20240323121305_init
│ │ │ │ └── migration.sql
│ │ │ ├── 20240324100733_add_merchant
│ │ │ │ └── migration.sql
│ │ │ ├── 20240324104524_add_merchant
│ │ │ │ └── migration.sql
│ │ │ ├── 20240324105137_add_password
│ │ │ │ └── migration.sql
│ │ │ ├── 20240324145646_added_balances_and_onramp
│ │ │ │ └── migration.sql
│ │ │ ├── 20240330154923_adds_p2p_transfer
│ │ │ │ └── migration.sql
│ │ │ └── migration_lock.toml
│ │ ├── schema.prisma
│ │ └── seed.ts
│ └── tsconfig.json
├── eslint-config
│ ├── README.md
│ ├── library.js
│ ├── next.js
│ ├── package.json
│ └── react-internal.js
├── store
│ ├── package.json
│ ├── src
│ │ ├── atoms
│ │ │ └── balance.ts
│ │ └── hooks
│ │ │ └── useBalance.ts
│ └── tsconfig.json
├── typescript-config
│ ├── base.json
│ ├── nextjs.json
│ ├── package.json
│ └── react-library.json
└── ui
│ ├── .eslintrc.js
│ ├── package.json
│ ├── src
│ ├── Appbar.tsx
│ ├── Center.tsx
│ ├── Select.tsx
│ ├── TextInput.tsx
│ ├── button.tsx
│ ├── card.tsx
│ └── code.tsx
│ ├── tsconfig.json
│ ├── tsconfig.lint.json
│ └── turbo
│ └── generators
│ ├── config.ts
│ └── templates
│ └── component.hbs
├── tsconfig.json
└── turbo.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // This configuration only applies to the package manager root.
2 | /** @type {import("eslint").Linter.Config} */
3 | module.exports = {
4 | ignorePatterns: ["apps/**", "packages/**"],
5 | extends: ["@repo/eslint-config/library.js"],
6 | parser: "@typescript-eslint/parser",
7 | parserOptions: {
8 | project: true,
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build on PR
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | - name: Use Node.js
14 | uses: actions/setup-node@v3
15 | with:
16 | node-version: '20'
17 |
18 | - name: Install Dependencies
19 | run: npm install
20 |
21 | - name: Generate prisma client
22 | run: npm run db:generate
23 |
24 | - name: Run Build
25 | run: npm run build
26 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Build and Deploy to Docker Hub
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | build-and-push:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Check Out Repo
13 | uses: actions/checkout@v2
14 |
15 | - name: Log in to Docker Hub
16 | uses: docker/login-action@v1
17 | with:
18 | username: ${{ secrets.DOCKER_USERNAME }}
19 | password: ${{ secrets.DOCKER_PASSWORD }}
20 |
21 | - name: Build and Push Docker image
22 | uses: docker/build-push-action@v2
23 | with:
24 | context: .
25 | file: ./docker/Dockerfile.user
26 | push: true
27 | tags: 100xdevs/week-18-class:latest # Replace with your Docker Hub username and repository
28 |
29 | - name: Verify Pushed Image
30 | run: docker pull 100xdevs/week-18-class:latest # Replace with your Docker Hub username and repository
31 |
32 | - name: Deploy to EC2
33 | uses: appleboy/ssh-action@master
34 | with:
35 | host: ${{ secrets.SSH_HOST }}
36 | username: ${{ secrets.SSH_USERNAME }}
37 | key: ${{ secrets.SSH_KEY }}
38 | script: |
39 | sudo docker pull 100xdevs/week-18-class:latest
40 | sudo docker stop web-app || true
41 | sudo docker rm web-app || true
42 | sudo docker run -d --name web-app -p 3005:3000 100xdevs/week-18-class:latest
--------------------------------------------------------------------------------
/.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 |
8 | # Local env files
9 | .env
10 | .env.local
11 | .env.development.local
12 | .env.test.local
13 | .env.production.local
14 |
15 | # Testing
16 | coverage
17 |
18 | # Turbo
19 | .turbo
20 |
21 | # Vercel
22 | .vercel
23 |
24 | # Build Outputs
25 | .next/
26 | out/
27 | build
28 | dist
29 |
30 |
31 | # Debug
32 | npm-debug.log*
33 | yarn-debug.log*
34 | yarn-error.log*
35 |
36 | # Misc
37 | .DS_Store
38 | *.pem
39 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/100xdevs-cohort-2/week-18-2-ci-cd/65bbb1fa52a6afe0d9916277c7c458ccebcaa05d/.npmrc
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | - Clone the repo
2 |
3 | ```jsx
4 | git clone https://github.com/100xdevs-cohort-2/week-17-final-code
5 | ```
6 |
7 | - npm install
8 | - Run postgres either locally or on the cloud (neon.tech)
9 |
10 | ```jsx
11 | docker run -e POSTGRES_PASSWORD=mysecretpassword -d -p 5432:5432 postgres
12 | ```
13 |
14 | - Copy over all .env.example files to .env
15 | - Update .env files everywhere with the right db url
16 | - Go to `packages/db`
17 | - npx prisma migrate dev
18 | - npx prisma db seed
19 | - Go to `apps/user-app` , run `npm run dev`
20 | - Try logging in using phone - 1111111111 , password - alice (See `seed.ts`)
--------------------------------------------------------------------------------
/apps/bank-webhook/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bank-webhook",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "npx esbuild ./src/index.ts --bundle --platform=node --outfile=dist/index.js",
8 | "start": "node dist/index.js",
9 | "dev": "npm run build && npm run start"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "dependencies": {
15 | "@repo/db": "*",
16 | "@types/express": "^4.17.21",
17 | "esbuild": "^0.20.2",
18 | "express": "^4.19.1"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/apps/bank-webhook/src/index.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import db from "@repo/db/client";
3 | const app = express();
4 |
5 | app.use(express.json())
6 |
7 | app.post("/hdfcWebhook", async (req, res) => {
8 | //TODO: Add zod validation here?
9 | //TODO: HDFC bank should ideally send us a secret so we know this is sent by them
10 | const paymentInformation: {
11 | token: string;
12 | userId: string;
13 | amount: string
14 | } = {
15 | token: req.body.token,
16 | userId: req.body.user_identifier,
17 | amount: req.body.amount
18 | };
19 |
20 | try {
21 | await db.$transaction([
22 | db.balance.updateMany({
23 | where: {
24 | userId: Number(paymentInformation.userId)
25 | },
26 | data: {
27 | amount: {
28 | // You can also get this from your DB
29 | increment: Number(paymentInformation.amount)
30 | }
31 | }
32 | }),
33 | db.onRampTransaction.updateMany({
34 | where: {
35 | token: paymentInformation.token
36 | },
37 | data: {
38 | status: "Success",
39 | }
40 | })
41 | ]);
42 |
43 | res.json({
44 | message: "Captured"
45 | })
46 | } catch(e) {
47 | console.error(e);
48 | res.status(411).json({
49 | message: "Error while processing webhook"
50 | })
51 | }
52 |
53 | })
54 |
55 | app.listen(3003);
--------------------------------------------------------------------------------
/apps/bank-webhook/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/base.json",
3 | "compilerOptions": {
4 | "outDir": "dist"
5 | },
6 | "include": ["src"],
7 | "exclude": ["node_modules", "dist"]
8 | }
9 |
--------------------------------------------------------------------------------
/apps/merchant-app/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /** @type {import("eslint").Linter.Config} */
2 | module.exports = {
3 | root: true,
4 | extends: ["@repo/eslint-config/next.js"],
5 | parser: "@typescript-eslint/parser",
6 | parserOptions: {
7 | project: true,
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/apps/merchant-app/README.md:
--------------------------------------------------------------------------------
1 | ## Getting Started
2 |
3 | First, run the development server:
4 |
5 | ```bash
6 | yarn dev
7 | ```
8 |
9 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
10 |
11 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
12 |
13 | To create [API routes](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) add an `api/` directory to the `app/` directory with a `route.ts` file. For individual endpoints, create a subfolder in the `api` directory, like `api/hello/route.ts` would map to [http://localhost:3000/api/hello](http://localhost:3000/api/hello).
14 |
15 | ## Learn More
16 |
17 | To learn more about Next.js, take a look at the following resources:
18 |
19 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
20 | - [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an interactive Next.js tutorial.
21 |
22 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
23 |
24 | ## Deploy on Vercel
25 |
26 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js.
27 |
28 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
29 |
--------------------------------------------------------------------------------
/apps/merchant-app/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth"
2 | import { authOptions } from "../../../../lib/auth"
3 |
4 | //@ts-ignore
5 | const handler = NextAuth(authOptions)
6 |
7 | export { handler as GET, handler as POST }
--------------------------------------------------------------------------------
/apps/merchant-app/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/100xdevs-cohort-2/week-18-2-ci-cd/65bbb1fa52a6afe0d9916277c7c458ccebcaa05d/apps/merchant-app/app/favicon.ico
--------------------------------------------------------------------------------
/apps/merchant-app/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/apps/merchant-app/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./globals.css";
2 | import type { Metadata } from "next";
3 | import { Inter } from "next/font/google";
4 | import { Providers } from "../provider";
5 |
6 | const inter = Inter({ subsets: ["latin"] });
7 |
8 | export const metadata: Metadata = {
9 | title: "Create Turborepo",
10 | description: "Generated by create turbo",
11 | };
12 |
13 | export default function RootLayout({
14 | children,
15 | }: {
16 | children: React.ReactNode;
17 | }): JSX.Element {
18 | return (
19 |
20 |
21 | {children}
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/apps/merchant-app/app/page.module.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | align-items: center;
6 | padding: 6rem;
7 | min-height: 100vh;
8 | }
9 |
10 | .vercelLogo {
11 | filter: invert(1);
12 | }
13 |
14 | .description {
15 | display: inherit;
16 | justify-content: inherit;
17 | align-items: inherit;
18 | font-size: 0.85rem;
19 | max-width: var(--max-width);
20 | width: 100%;
21 | z-index: 2;
22 | font-family: var(--font-mono);
23 | }
24 |
25 | .description a {
26 | display: flex;
27 | justify-content: center;
28 | align-items: center;
29 | gap: 0.5rem;
30 | }
31 |
32 | .description p {
33 | position: relative;
34 | margin: 0;
35 | padding: 1rem;
36 | background-color: rgba(var(--callout-rgb), 0.5);
37 | border: 1px solid rgba(var(--callout-border-rgb), 0.3);
38 | border-radius: var(--border-radius);
39 | }
40 |
41 | .code {
42 | font-weight: 700;
43 | font-family: var(--font-mono);
44 | }
45 |
46 | .hero {
47 | display: flex;
48 | position: relative;
49 | place-items: center;
50 | }
51 |
52 | .heroContent {
53 | display: flex;
54 | position: relative;
55 | z-index: 0;
56 | padding-bottom: 4rem;
57 | flex-direction: column;
58 | gap: 2rem;
59 | justify-content: space-between;
60 | align-items: center;
61 | width: auto;
62 | font-family: system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial,
63 | "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
64 | "Segoe UI Symbol", "Noto Color Emoji";
65 | padding-top: 48px;
66 |
67 | @media (min-width: 768px) {
68 | padding-top: 4rem;
69 | padding-bottom: 6rem;
70 | }
71 | @media (min-width: 1024px) {
72 | padding-top: 5rem;
73 | padding-bottom: 8rem;
74 | }
75 | }
76 |
77 | .logos {
78 | display: flex;
79 | z-index: 50;
80 | justify-content: center;
81 | align-items: center;
82 | width: 100%;
83 | }
84 |
85 | .grid {
86 | display: grid;
87 | grid-template-columns: repeat(4, minmax(25%, auto));
88 | max-width: 100%;
89 | width: var(--max-width);
90 | }
91 |
92 | .card {
93 | padding: 1rem 1.2rem;
94 | border-radius: var(--border-radius);
95 | background: rgba(var(--card-rgb), 0);
96 | border: 1px solid rgba(var(--card-border-rgb), 0);
97 | transition: background 200ms, border 200ms;
98 | }
99 |
100 | .card span {
101 | display: inline-block;
102 | transition: transform 200ms;
103 | }
104 |
105 | .card h2 {
106 | font-weight: 600;
107 | margin-bottom: 0.7rem;
108 | }
109 |
110 | .card p {
111 | margin: 0;
112 | opacity: 0.6;
113 | font-size: 0.9rem;
114 | line-height: 1.5;
115 | max-width: 30ch;
116 | }
117 |
118 | @media (prefers-reduced-motion) {
119 | .card:hover span {
120 | transform: none;
121 | }
122 | }
123 |
124 | /* Mobile */
125 | @media (max-width: 700px) {
126 | .content {
127 | padding: 4rem;
128 | }
129 |
130 | .grid {
131 | grid-template-columns: 1fr;
132 | margin-bottom: 120px;
133 | max-width: 320px;
134 | text-align: center;
135 | }
136 |
137 | .card {
138 | padding: 1rem 2.5rem;
139 | }
140 |
141 | .card h2 {
142 | margin-bottom: 0.5rem;
143 | }
144 |
145 | .center {
146 | padding: 8rem 0 6rem;
147 | }
148 |
149 | .center::before {
150 | transform: none;
151 | height: 300px;
152 | }
153 |
154 | .description {
155 | font-size: 0.8rem;
156 | }
157 |
158 | .description a {
159 | padding: 1rem;
160 | }
161 |
162 | .description p,
163 | .description div {
164 | display: flex;
165 | justify-content: center;
166 | position: fixed;
167 | width: 100%;
168 | }
169 |
170 | .description p {
171 | align-items: center;
172 | inset: 0 0 auto;
173 | padding: 2rem 1rem 1.4rem;
174 | border-radius: 0;
175 | border: none;
176 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
177 | background: linear-gradient(
178 | to bottom,
179 | rgba(var(--background-start-rgb), 1),
180 | rgba(var(--callout-rgb), 0.5)
181 | );
182 | background-clip: padding-box;
183 | backdrop-filter: blur(24px);
184 | }
185 |
186 | .description div {
187 | align-items: flex-end;
188 | pointer-events: none;
189 | inset: auto 0 0;
190 | padding: 2rem;
191 | height: 200px;
192 | background: linear-gradient(
193 | to bottom,
194 | transparent 0%,
195 | rgb(var(--background-end-rgb)) 40%
196 | );
197 | z-index: 1;
198 | }
199 | }
200 |
201 | /* Enable hover only on non-touch devices */
202 | @media (hover: hover) and (pointer: fine) {
203 | .card:hover {
204 | background: rgba(var(--card-rgb), 0.1);
205 | border: 1px solid rgba(var(--card-border-rgb), 0.15);
206 | }
207 |
208 | .card:hover span {
209 | transform: translateX(4px);
210 | }
211 | }
212 |
213 | .circles {
214 | position: absolute;
215 | min-width: 614px;
216 | min-height: 614px;
217 | pointer-events: none;
218 | }
219 |
220 | .logo {
221 | z-index: 50;
222 | width: 120px;
223 | height: 120px;
224 | }
225 |
226 | .logoGradientContainer {
227 | display: flex;
228 | position: absolute;
229 | z-index: 50;
230 | justify-content: center;
231 | align-items: center;
232 | width: 16rem;
233 | height: 16rem;
234 | }
235 |
236 | .turborepoWordmarkContainer {
237 | display: flex;
238 | z-index: 50;
239 | padding-left: 1.5rem;
240 | padding-right: 1.5rem;
241 | flex-direction: column;
242 | gap: 1.25rem;
243 | justify-content: center;
244 | align-items: center;
245 | text-align: center;
246 |
247 | @media (min-width: 1024px) {
248 | gap: 1.5rem;
249 | }
250 | }
251 |
252 | .turborepoWordmark {
253 | width: 160px;
254 | fill: white;
255 |
256 | @media (min-width: 768px) {
257 | width: 200px;
258 | }
259 | }
260 |
261 | .code {
262 | font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
263 | monospace;
264 | font-weight: 700;
265 | }
266 |
267 | /* Tablet and Smaller Desktop */
268 | @media (min-width: 701px) and (max-width: 1120px) {
269 | .grid {
270 | grid-template-columns: repeat(2, 50%);
271 | }
272 | }
273 |
274 | /* Gradients */
275 | .gradient {
276 | position: absolute;
277 | mix-blend-mode: normal;
278 | will-change: filter;
279 | pointer-events: none;
280 | }
281 |
282 | .gradientSmall {
283 | filter: blur(32px);
284 | }
285 |
286 | .gradientLarge {
287 | filter: blur(75px);
288 | }
289 |
290 | .glowConic {
291 | background-image: var(--glow-conic);
292 | }
293 |
294 | .logoGradient {
295 | opacity: 0.9;
296 | width: 120px;
297 | height: 120px;
298 | }
299 |
300 | .backgroundGradient {
301 | top: -500px;
302 | width: 1000px;
303 | height: 1000px;
304 | opacity: 0.15;
305 | }
306 |
307 | .button {
308 | background-color: #ffffff;
309 | border-radius: 8px;
310 | border-style: none;
311 | box-sizing: border-box;
312 | color: #000000;
313 | cursor: pointer;
314 | display: inline-block;
315 | font-size: 16px;
316 | height: 40px;
317 | line-height: 20px;
318 | list-style: none;
319 | margin: 0;
320 | outline: none;
321 | padding: 10px 16px;
322 | position: relative;
323 | text-align: center;
324 | text-decoration: none;
325 | transition: color 100ms;
326 | vertical-align: baseline;
327 | user-select: none;
328 | -webkit-user-select: none;
329 | touch-action: manipulation;
330 | }
331 |
332 | .button:hover,
333 | .button:focus {
334 | background-color: #e5e4e2;
335 | }
336 |
--------------------------------------------------------------------------------
/apps/merchant-app/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useBalance } from "@repo/store/balance";
4 |
5 | export default function() {
6 | const balance = useBalance();
7 | return
8 | hi there {balance}
9 |
10 | }
--------------------------------------------------------------------------------
/apps/merchant-app/lib/auth.ts:
--------------------------------------------------------------------------------
1 | import GoogleProvider from "next-auth/providers/google";
2 | import db from "@repo/db/client";
3 |
4 | export const authOptions = {
5 | providers: [
6 | GoogleProvider({
7 | clientId: process.env.GOOGLE_CLIENT_ID || "",
8 | clientSecret: process.env.GOOGLE_CLIENT_SECRET || ""
9 | })
10 | ],
11 | callbacks: {
12 | async signIn({ user, account }: {
13 | user: {
14 | email: string;
15 | name: string
16 | },
17 | account: {
18 | provider: "google" | "github"
19 | }
20 | }) {
21 | console.log("hi signin")
22 | if (!user || !user.email) {
23 | return false;
24 | }
25 |
26 | await db.merchant.upsert({
27 | select: {
28 | id: true
29 | },
30 | where: {
31 | email: user.email
32 | },
33 | create: {
34 | email: user.email,
35 | name: user.name,
36 | auth_type: account.provider === "google" ? "Google" : "Github" // Use a prisma type here
37 | },
38 | update: {
39 | name: user.name,
40 | auth_type: account.provider === "google" ? "Google" : "Github" // Use a prisma type here
41 | }
42 | });
43 |
44 | return true;
45 | }
46 | },
47 | secret: process.env.NEXTAUTH_SECRET || "secret"
48 | }
--------------------------------------------------------------------------------
/apps/merchant-app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/apps/merchant-app/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | module.exports = {
3 | transpilePackages: ["@repo/ui"],
4 | };
5 |
--------------------------------------------------------------------------------
/apps/merchant-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "eslint . --max-warnings 0"
10 | },
11 | "dependencies": {
12 | "@repo/db": "*",
13 | "@repo/store": "*",
14 | "@repo/ui": "*",
15 | "next": "^14.1.1",
16 | "next-auth": "^4.24.7",
17 | "react": "^18.2.0",
18 | "react-dom": "^18.2.0",
19 | "recoil": "^0.7.7"
20 | },
21 | "devDependencies": {
22 | "@next/eslint-plugin-next": "^14.1.1",
23 | "@repo/eslint-config": "*",
24 | "@repo/typescript-config": "*",
25 | "@types/eslint": "^8.56.5",
26 | "@types/node": "^20.11.24",
27 | "@types/react": "^18.2.61",
28 | "@types/react-dom": "^18.2.19",
29 | "eslint": "^8.57.0",
30 | "typescript": "^5.3.3"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/apps/merchant-app/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/apps/merchant-app/provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { RecoilRoot } from "recoil";
3 | import { SessionProvider } from "next-auth/react";
4 | export const Providers = ({children}: {children: React.ReactNode}) => {
5 | return
6 |
7 | {children}
8 |
9 |
10 | }
--------------------------------------------------------------------------------
/apps/merchant-app/public/circles.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/merchant-app/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/merchant-app/public/turborepo.svg:
--------------------------------------------------------------------------------
1 |
33 |
--------------------------------------------------------------------------------
/apps/merchant-app/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/merchant-app/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "../../packages/ui/**/*.{js,ts,jsx,tsx,mdx}"
8 | ],
9 | theme: {
10 | extend: {},
11 | },
12 | plugins: [],
13 | }
--------------------------------------------------------------------------------
/apps/merchant-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "plugins": [
5 | {
6 | "name": "next"
7 | }
8 | ]
9 | },
10 | "include": [
11 | "next-env.d.ts",
12 | "next.config.js",
13 | "**/*.ts",
14 | "**/*.tsx",
15 | ".next/types/**/*.ts"
16 | ],
17 | "exclude": ["node_modules"]
18 | }
19 |
--------------------------------------------------------------------------------
/apps/user-app/.env.example:
--------------------------------------------------------------------------------
1 | JWT_SECRET=test
2 | NEXTAUTH_URL=http://localhost:3001
3 |
--------------------------------------------------------------------------------
/apps/user-app/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /** @type {import("eslint").Linter.Config} */
2 | module.exports = {
3 | root: true,
4 | extends: ["@repo/eslint-config/next.js"],
5 | parser: "@typescript-eslint/parser",
6 | parserOptions: {
7 | project: true,
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/apps/user-app/README.md:
--------------------------------------------------------------------------------
1 | ## Getting Started
2 |
3 | First, run the development server:
4 |
5 | ```bash
6 | yarn dev
7 | ```
8 |
9 | Open [http://localhost:3001](http://localhost:3001) with your browser to see the result.
10 |
11 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
12 |
13 | To create [API routes](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) add an `api/` directory to the `app/` directory with a `route.ts` file. For individual endpoints, create a subfolder in the `api` directory, like `api/hello/route.ts` would map to [http://localhost:3001/api/hello](http://localhost:3001/api/hello).
14 |
15 | ## Learn More
16 |
17 | To learn more about Next.js, take a look at the following resources:
18 |
19 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
20 | - [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an interactive Next.js tutorial.
21 |
22 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
23 |
24 | ## Deploy on Vercel
25 |
26 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js.
27 |
28 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
29 |
--------------------------------------------------------------------------------
/apps/user-app/app/(dashboard)/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 |
2 | export default function() {
3 | return
4 | Dashboard
5 |
6 | }
--------------------------------------------------------------------------------
/apps/user-app/app/(dashboard)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { SidebarItem } from "../../components/SidebarItem";
2 |
3 | export default function Layout({
4 | children,
5 | }: {
6 | children: React.ReactNode;
7 | }): JSX.Element {
8 | return (
9 |
10 |
11 |
12 | } title="Home" />
13 | } title="Transfer" />
14 | } title="Transactions" />
15 | } title="P2P Transfer" />
16 |
17 |
18 | {children}
19 |
20 | );
21 | }
22 |
23 | // Icons Fetched from https://heroicons.com/
24 | function HomeIcon() {
25 | return
28 | }
29 |
30 | function P2PTransferIcon() {
31 | return
34 |
35 | }
36 | function TransferIcon() {
37 | return
40 | }
41 |
42 | function TransactionsIcon() {
43 | return
46 |
47 | }
--------------------------------------------------------------------------------
/apps/user-app/app/(dashboard)/p2p/page.tsx:
--------------------------------------------------------------------------------
1 | import { SendCard } from "../../../components/SendCard";
2 |
3 | export default function() {
4 | return
5 |
6 |
7 | }
--------------------------------------------------------------------------------
/apps/user-app/app/(dashboard)/transactions/page.tsx:
--------------------------------------------------------------------------------
1 |
2 | export default function() {
3 | return
4 | Transactions
5 |
6 | }
--------------------------------------------------------------------------------
/apps/user-app/app/(dashboard)/transfer/page.tsx:
--------------------------------------------------------------------------------
1 | import prisma from "@repo/db/client";
2 | import { AddMoney } from "../../../components/AddMoneyCard";
3 | import { BalanceCard } from "../../../components/BalanceCard";
4 | import { OnRampTransactions } from "../../../components/OnRampTransactions";
5 | import { getServerSession } from "next-auth";
6 | import { authOptions } from "../../lib/auth";
7 |
8 | async function getBalance() {
9 | const session = await getServerSession(authOptions);
10 | const balance = await prisma.balance.findFirst({
11 | where: {
12 | userId: Number(session?.user?.id)
13 | }
14 | });
15 | return {
16 | amount: balance?.amount || 0,
17 | locked: balance?.locked || 0
18 | }
19 | }
20 |
21 | async function getOnRampTransactions() {
22 | const session = await getServerSession(authOptions);
23 | const txns = await prisma.onRampTransaction.findMany({
24 | where: {
25 | userId: Number(session?.user?.id)
26 | }
27 | });
28 | return txns.map(t => ({
29 | time: t.startTime,
30 | amount: t.amount,
31 | status: t.status,
32 | provider: t.provider
33 | }))
34 | }
35 |
36 | export default async function() {
37 | const balance = await getBalance();
38 | const transactions = await getOnRampTransactions();
39 |
40 | return
41 | hi
42 |
43 | Transfer
44 |
45 |
56 |
57 | }
--------------------------------------------------------------------------------
/apps/user-app/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth"
2 | import { authOptions } from "../../../lib/auth"
3 |
4 | const handler = NextAuth(authOptions)
5 |
6 | export { handler as GET, handler as POST }
--------------------------------------------------------------------------------
/apps/user-app/app/api/user/route.ts:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth"
2 | import { NextResponse } from "next/server";
3 | import { authOptions } from "../../lib/auth";
4 |
5 | export const GET = async () => {
6 | const session = await getServerSession(authOptions);
7 | if (session.user) {
8 | return NextResponse.json({
9 | user: session.user
10 | })
11 | }
12 | return NextResponse.json({
13 | message: "You are not logged in"
14 | }, {
15 | status: 403
16 | })
17 | }
--------------------------------------------------------------------------------
/apps/user-app/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/100xdevs-cohort-2/week-18-2-ci-cd/65bbb1fa52a6afe0d9916277c7c458ccebcaa05d/apps/user-app/app/favicon.ico
--------------------------------------------------------------------------------
/apps/user-app/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/apps/user-app/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./globals.css";
2 | import type { Metadata } from "next";
3 | import { Inter } from "next/font/google";
4 | import { Providers } from "../provider";
5 | import { AppbarClient } from "../components/AppbarClient";
6 |
7 | const inter = Inter({ subsets: ["latin"] });
8 |
9 | export const metadata: Metadata = {
10 | title: "Wallet",
11 | description: "Simple wallet app",
12 | };
13 |
14 | export default function RootLayout({
15 | children,
16 | }: {
17 | children: React.ReactNode;
18 | }): JSX.Element {
19 | return (
20 |
21 |
22 |
23 |
24 |
25 | {children}
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/apps/user-app/app/lib/actions/createOnRamptxn.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { getServerSession } from "next-auth";
4 | import { authOptions } from "../auth";
5 | import prisma from "@repo/db/client";
6 |
7 | export async function createOnRampTransaction(amount: number, provider: string) {
8 | const session = await getServerSession(authOptions);
9 | const token = Math.random().toString();
10 | const userId = session.user.id;
11 | if (!userId) {
12 | return {
13 | message: "User not logged in"
14 | }
15 | }
16 | await prisma.onRampTransaction.create({
17 | data: {
18 | userId: Number(userId), // 1
19 | amount: amount,
20 | status: "Processing",
21 | startTime: new Date(),
22 | provider,
23 | token: token
24 | }
25 | })
26 |
27 | return {
28 | message: "On ramp transaction added"
29 | }
30 | }
--------------------------------------------------------------------------------
/apps/user-app/app/lib/actions/p2pTransfer.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 | import { getServerSession } from "next-auth";
3 | import { authOptions } from "../auth";
4 | import prisma from "@repo/db/client";
5 |
6 | export async function p2pTransfer(to: string, amount: number) {
7 | const session = await getServerSession(authOptions);
8 | const from = session?.user?.id;
9 | if (!from) {
10 | return {
11 | message: "Error while sending"
12 | }
13 | }
14 | const toUser = await prisma.user.findFirst({
15 | where: {
16 | number: to
17 | }
18 | });
19 |
20 | if (!toUser) {
21 | return {
22 | message: "User not found"
23 | }
24 | }
25 | await prisma.$transaction(async (tx) => {
26 | await tx.$queryRaw`SELECT * FROM "Balance" WHERE "userId" = ${Number(from)} FOR UPDATE`;
27 |
28 | const fromBalance = await tx.balance.findUnique({
29 | where: { userId: Number(from) },
30 | });
31 | if (!fromBalance || fromBalance.amount < amount) {
32 | throw new Error('Insufficient funds');
33 | }
34 |
35 | await tx.balance.update({
36 | where: { userId: Number(from) },
37 | data: { amount: { decrement: amount } },
38 | });
39 |
40 | await tx.balance.update({
41 | where: { userId: toUser.id },
42 | data: { amount: { increment: amount } },
43 | });
44 |
45 | await tx.p2pTransfer.create({
46 | data: {
47 | fromUserId: Number(from),
48 | toUserId: toUser.id,
49 | amount,
50 | timestamp: new Date()
51 | }
52 | })
53 | // locking
54 | });
55 | }
--------------------------------------------------------------------------------
/apps/user-app/app/lib/auth.ts:
--------------------------------------------------------------------------------
1 | import db from "@repo/db/client";
2 | import CredentialsProvider from "next-auth/providers/credentials"
3 | import bcrypt from "bcrypt";
4 |
5 | export const authOptions = {
6 | providers: [
7 | CredentialsProvider({
8 | name: 'Credentials',
9 | credentials: {
10 | phone: { label: "Phone number", type: "text", placeholder: "1231231231", required: true },
11 | password: { label: "Password", type: "password", required: true }
12 | },
13 | // TODO: User credentials type from next-aut
14 | async authorize(credentials: any) {
15 | // Do zod validation, OTP validation here
16 | const hashedPassword = await bcrypt.hash(credentials.password, 10);
17 | const existingUser = await db.user.findFirst({
18 | where: {
19 | number: credentials.phone
20 | }
21 | });
22 |
23 | if (existingUser) {
24 | const passwordValidation = await bcrypt.compare(credentials.password, existingUser.password);
25 | if (passwordValidation) {
26 | return {
27 | id: existingUser.id.toString(),
28 | name: existingUser.name,
29 | email: existingUser.number
30 | }
31 | }
32 | return null;
33 | }
34 |
35 | try {
36 | const user = await db.user.create({
37 | data: {
38 | number: credentials.phone,
39 | password: hashedPassword
40 | }
41 | });
42 |
43 | return {
44 | id: user.id.toString(),
45 | name: user.name,
46 | email: user.number
47 | }
48 | } catch(e) {
49 | console.error(e);
50 | }
51 |
52 | return null
53 | },
54 | })
55 | ],
56 | secret: process.env.JWT_SECRET || "secret",
57 | callbacks: {
58 | // TODO: can u fix the type here? Using any is bad
59 | async session({ token, session }: any) {
60 | session.user.id = token.sub
61 |
62 | return session
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/apps/user-app/app/page.module.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | align-items: center;
6 | padding: 6rem;
7 | min-height: 100vh;
8 | }
9 |
10 | .vercelLogo {
11 | filter: invert(1);
12 | }
13 |
14 | .description {
15 | display: inherit;
16 | justify-content: inherit;
17 | align-items: inherit;
18 | font-size: 0.85rem;
19 | max-width: var(--max-width);
20 | width: 100%;
21 | z-index: 2;
22 | font-family: var(--font-mono);
23 | }
24 |
25 | .description a {
26 | display: flex;
27 | justify-content: center;
28 | align-items: center;
29 | gap: 0.5rem;
30 | }
31 |
32 | .description p {
33 | position: relative;
34 | margin: 0;
35 | padding: 1rem;
36 | background-color: rgba(var(--callout-rgb), 0.5);
37 | border: 1px solid rgba(var(--callout-border-rgb), 0.3);
38 | border-radius: var(--border-radius);
39 | }
40 |
41 | .code {
42 | font-weight: 700;
43 | font-family: var(--font-mono);
44 | }
45 |
46 | .hero {
47 | display: flex;
48 | position: relative;
49 | place-items: center;
50 | }
51 |
52 | .heroContent {
53 | display: flex;
54 | position: relative;
55 | z-index: 0;
56 | padding-bottom: 4rem;
57 | flex-direction: column;
58 | gap: 2rem;
59 | justify-content: space-between;
60 | align-items: center;
61 | width: auto;
62 | font-family: system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial,
63 | "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
64 | "Segoe UI Symbol", "Noto Color Emoji";
65 | padding-top: 48px;
66 |
67 | @media (min-width: 768px) {
68 | padding-top: 4rem;
69 | padding-bottom: 6rem;
70 | }
71 | @media (min-width: 1024px) {
72 | padding-top: 5rem;
73 | padding-bottom: 8rem;
74 | }
75 | }
76 |
77 | .logos {
78 | display: flex;
79 | z-index: 50;
80 | justify-content: center;
81 | align-items: center;
82 | width: 100%;
83 | }
84 |
85 | .grid {
86 | display: grid;
87 | grid-template-columns: repeat(4, minmax(25%, auto));
88 | max-width: 100%;
89 | width: var(--max-width);
90 | }
91 |
92 | .card {
93 | padding: 1rem 1.2rem;
94 | border-radius: var(--border-radius);
95 | background: rgba(var(--card-rgb), 0);
96 | border: 1px solid rgba(var(--card-border-rgb), 0);
97 | transition: background 200ms, border 200ms;
98 | }
99 |
100 | .card span {
101 | display: inline-block;
102 | transition: transform 200ms;
103 | }
104 |
105 | .card h2 {
106 | font-weight: 600;
107 | margin-bottom: 0.7rem;
108 | }
109 |
110 | .card p {
111 | margin: 0;
112 | opacity: 0.6;
113 | font-size: 0.9rem;
114 | line-height: 1.5;
115 | max-width: 30ch;
116 | }
117 |
118 | @media (prefers-reduced-motion) {
119 | .card:hover span {
120 | transform: none;
121 | }
122 | }
123 |
124 | /* Mobile */
125 | @media (max-width: 700px) {
126 | .content {
127 | padding: 4rem;
128 | }
129 |
130 | .grid {
131 | grid-template-columns: 1fr;
132 | margin-bottom: 120px;
133 | max-width: 320px;
134 | text-align: center;
135 | }
136 |
137 | .card {
138 | padding: 1rem 2.5rem;
139 | }
140 |
141 | .card h2 {
142 | margin-bottom: 0.5rem;
143 | }
144 |
145 | .center {
146 | padding: 8rem 0 6rem;
147 | }
148 |
149 | .center::before {
150 | transform: none;
151 | height: 300px;
152 | }
153 |
154 | .description {
155 | font-size: 0.8rem;
156 | }
157 |
158 | .description a {
159 | padding: 1rem;
160 | }
161 |
162 | .description p,
163 | .description div {
164 | display: flex;
165 | justify-content: center;
166 | position: fixed;
167 | width: 100%;
168 | }
169 |
170 | .description p {
171 | align-items: center;
172 | inset: 0 0 auto;
173 | padding: 2rem 1rem 1.4rem;
174 | border-radius: 0;
175 | border: none;
176 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
177 | background: linear-gradient(
178 | to bottom,
179 | rgba(var(--background-start-rgb), 1),
180 | rgba(var(--callout-rgb), 0.5)
181 | );
182 | background-clip: padding-box;
183 | backdrop-filter: blur(24px);
184 | }
185 |
186 | .description div {
187 | align-items: flex-end;
188 | pointer-events: none;
189 | inset: auto 0 0;
190 | padding: 2rem;
191 | height: 200px;
192 | background: linear-gradient(
193 | to bottom,
194 | transparent 0%,
195 | rgb(var(--background-end-rgb)) 40%
196 | );
197 | z-index: 1;
198 | }
199 | }
200 |
201 | /* Enable hover only on non-touch devices */
202 | @media (hover: hover) and (pointer: fine) {
203 | .card:hover {
204 | background: rgba(var(--card-rgb), 0.1);
205 | border: 1px solid rgba(var(--card-border-rgb), 0.15);
206 | }
207 |
208 | .card:hover span {
209 | transform: translateX(4px);
210 | }
211 | }
212 |
213 | .circles {
214 | position: absolute;
215 | min-width: 614px;
216 | min-height: 614px;
217 | pointer-events: none;
218 | }
219 |
220 | .logo {
221 | z-index: 50;
222 | width: 120px;
223 | height: 120px;
224 | }
225 |
226 | .logoGradientContainer {
227 | display: flex;
228 | position: absolute;
229 | z-index: 50;
230 | justify-content: center;
231 | align-items: center;
232 | width: 16rem;
233 | height: 16rem;
234 | }
235 |
236 | .turborepoWordmarkContainer {
237 | display: flex;
238 | z-index: 50;
239 | padding-left: 1.5rem;
240 | padding-right: 1.5rem;
241 | flex-direction: column;
242 | gap: 1.25rem;
243 | justify-content: center;
244 | align-items: center;
245 | text-align: center;
246 |
247 | @media (min-width: 1024px) {
248 | gap: 1.5rem;
249 | }
250 | }
251 |
252 | .turborepoWordmark {
253 | width: 160px;
254 | fill: white;
255 |
256 | @media (min-width: 768px) {
257 | width: 200px;
258 | }
259 | }
260 |
261 | .code {
262 | font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
263 | monospace;
264 | font-weight: 700;
265 | }
266 |
267 | /* Tablet and Smaller Desktop */
268 | @media (min-width: 701px) and (max-width: 1120px) {
269 | .grid {
270 | grid-template-columns: repeat(2, 50%);
271 | }
272 | }
273 |
274 | /* Gradients */
275 | .gradient {
276 | position: absolute;
277 | mix-blend-mode: normal;
278 | will-change: filter;
279 | pointer-events: none;
280 | }
281 |
282 | .gradientSmall {
283 | filter: blur(32px);
284 | }
285 |
286 | .gradientLarge {
287 | filter: blur(75px);
288 | }
289 |
290 | .glowConic {
291 | background-image: var(--glow-conic);
292 | }
293 |
294 | .logoGradient {
295 | opacity: 0.9;
296 | width: 120px;
297 | height: 120px;
298 | }
299 |
300 | .backgroundGradient {
301 | top: -500px;
302 | width: 1000px;
303 | height: 1000px;
304 | opacity: 0.15;
305 | }
306 |
307 | .button {
308 | background-color: #ffffff;
309 | border-radius: 8px;
310 | border-style: none;
311 | box-sizing: border-box;
312 | color: #000000;
313 | cursor: pointer;
314 | display: inline-block;
315 | font-size: 16px;
316 | height: 40px;
317 | line-height: 20px;
318 | list-style: none;
319 | margin: 0;
320 | outline: none;
321 | padding: 10px 16px;
322 | position: relative;
323 | text-align: center;
324 | text-decoration: none;
325 | transition: color 100ms;
326 | vertical-align: baseline;
327 | user-select: none;
328 | -webkit-user-select: none;
329 | touch-action: manipulation;
330 | }
331 |
332 | .button:hover,
333 | .button:focus {
334 | background-color: #e5e4e2;
335 | }
336 |
--------------------------------------------------------------------------------
/apps/user-app/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth";
2 | import { redirect } from 'next/navigation'
3 | import { authOptions } from "./lib/auth";
4 |
5 | export default async function Page() {
6 | const session = await getServerSession(authOptions);
7 | if (session?.user) {
8 | redirect('/dashboard')
9 | } else {
10 | redirect('/api/auth/signin')
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/apps/user-app/components/AddMoneyCard.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { Button } from "@repo/ui/button";
3 | import { Card } from "@repo/ui/card";
4 | import { Center } from "@repo/ui/center";
5 | import { Select } from "@repo/ui/select";
6 | import { useState } from "react";
7 | import { TextInput } from "@repo/ui/textinput";
8 | import { createOnRampTransaction } from "../app/lib/actions/createOnRamptxn";
9 |
10 | const SUPPORTED_BANKS = [{
11 | name: "HDFC Bank",
12 | redirectUrl: "https://netbanking.hdfcbank.com"
13 | }, {
14 | name: "Axis Bank",
15 | redirectUrl: "https://www.axisbank.com/"
16 | }];
17 |
18 | export const AddMoney = () => {
19 | const [redirectUrl, setRedirectUrl] = useState(SUPPORTED_BANKS[0]?.redirectUrl);
20 | const [amount, setAmount] = useState(0);
21 | const [provider, setProvider] = useState(SUPPORTED_BANKS[0]?.name || "");
22 |
23 | return
24 |
25 |
{
26 | setAmount(Number(value))
27 | }} />
28 |
29 | Bank
30 |
31 |
47 |
48 | }
--------------------------------------------------------------------------------
/apps/user-app/components/AppbarClient.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { signIn, signOut, useSession } from "next-auth/react";
3 | import { Appbar } from "@repo/ui/appbar";
4 | import { useRouter } from "next/navigation";
5 |
6 | export function AppbarClient() {
7 | const session = useSession();
8 | const router = useRouter();
9 |
10 | return (
11 |
12 |
{
13 | await signOut()
14 | router.push("/api/auth/signin")
15 | }} user={session.data?.user} />
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/apps/user-app/components/BalanceCard.tsx:
--------------------------------------------------------------------------------
1 | import { Card } from "@repo/ui/card";
2 |
3 | export const BalanceCard = ({amount, locked}: {
4 | amount: number;
5 | locked: number;
6 | }) => {
7 | return
8 |
9 |
10 | Unlocked balance
11 |
12 |
13 | {amount / 100} INR
14 |
15 |
16 |
17 |
18 | Total Locked Balance
19 |
20 |
21 | {locked / 100} INR
22 |
23 |
24 |
25 |
26 | Total Balance
27 |
28 |
29 | {(locked + amount) / 100} INR
30 |
31 |
32 |
33 | }
--------------------------------------------------------------------------------
/apps/user-app/components/OnRampTransactions.tsx:
--------------------------------------------------------------------------------
1 | import { Card } from "@repo/ui/card"
2 |
3 | export const OnRampTransactions = ({
4 | transactions
5 | }: {
6 | transactions: {
7 | time: Date,
8 | amount: number,
9 | // TODO: Can the type of `status` be more specific?
10 | status: string,
11 | provider: string
12 | }[]
13 | }) => {
14 | if (!transactions.length) {
15 | return
16 |
17 | No Recent transactions
18 |
19 |
20 | }
21 | return
22 |
23 | {transactions.map(t =>
24 |
25 |
26 | Received INR
27 |
28 |
29 | {t.time.toDateString()}
30 |
31 |
32 |
33 | + Rs {t.amount / 100}
34 |
35 |
36 |
)}
37 |
38 |
39 | }
--------------------------------------------------------------------------------
/apps/user-app/components/SendCard.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { Button } from "@repo/ui/button";
3 | import { Card } from "@repo/ui/card";
4 | import { Center } from "@repo/ui/center";
5 | import { TextInput } from "@repo/ui/textinput";
6 | import { useState } from "react";
7 | import { p2pTransfer } from "../app/lib/actions/p2pTransfer";
8 |
9 | export function SendCard() {
10 | const [number, setNumber] = useState("");
11 | const [amount, setAmount] = useState("");
12 |
13 | return
14 |
15 |
16 |
17 |
{
18 | setNumber(value)
19 | }} />
20 | {
21 | setAmount(value)
22 | }} />
23 |
24 |
27 |
28 |
29 |
30 |
31 |
32 | }
--------------------------------------------------------------------------------
/apps/user-app/components/SidebarItem.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { usePathname, useRouter } from "next/navigation";
3 | import React from "react";
4 |
5 | export const SidebarItem = ({ href, title, icon }: { href: string; title: string; icon: React.ReactNode }) => {
6 | const router = useRouter();
7 | const pathname = usePathname()
8 | const selected = pathname === href
9 |
10 | return {
11 | router.push(href);
12 | }}>
13 |
14 | {icon}
15 |
16 |
17 | {title}
18 |
19 |
20 | }
--------------------------------------------------------------------------------
/apps/user-app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/apps/user-app/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | module.exports = {
3 | transpilePackages: ["@repo/ui"],
4 | };
5 |
--------------------------------------------------------------------------------
/apps/user-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --port 3001",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "eslint . --max-warnings 0"
10 | },
11 | "dependencies": {
12 | "@repo/store": "*",
13 | "@repo/ui": "*",
14 | "@types/bcrypt": "^5.0.2",
15 | "bcrypt": "^5.1.1",
16 | "next": "^14.1.1",
17 | "next-auth": "^4.24.7",
18 | "react": "^18.2.0",
19 | "react-dom": "^18.2.0"
20 | },
21 | "devDependencies": {
22 | "@next/eslint-plugin-next": "^14.1.1",
23 | "@repo/eslint-config": "*",
24 | "@repo/typescript-config": "*",
25 | "@types/eslint": "^8.56.5",
26 | "@types/node": "^20.11.24",
27 | "@types/react": "^18.2.61",
28 | "@types/react-dom": "^18.2.19",
29 | "autoprefixer": "^10.4.19",
30 | "eslint": "^8.57.0",
31 | "postcss": "^8.4.38",
32 | "tailwindcss": "^3.4.1",
33 | "typescript": "^5.3.3"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/apps/user-app/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/apps/user-app/provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { RecoilRoot } from "recoil";
3 | import { SessionProvider } from "next-auth/react";
4 |
5 | export const Providers = ({children}: {children: React.ReactNode}) => {
6 | return
7 |
8 | {children}
9 |
10 |
11 | }
--------------------------------------------------------------------------------
/apps/user-app/public/circles.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/user-app/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/user-app/public/turborepo.svg:
--------------------------------------------------------------------------------
1 |
33 |
--------------------------------------------------------------------------------
/apps/user-app/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/user-app/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "../../packages/ui/**/*.{js,ts,jsx,tsx,mdx}"
8 | ],
9 | theme: {
10 | extend: {},
11 | },
12 | plugins: [],
13 | }
--------------------------------------------------------------------------------
/apps/user-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "plugins": [
5 | {
6 | "name": "next"
7 | }
8 | ]
9 | },
10 | "include": [
11 | "next-env.d.ts",
12 | "next.config.js",
13 | "**/*.ts",
14 | "**/*.tsx",
15 | ".next/types/**/*.ts"
16 | ],
17 | "exclude": ["node_modules"]
18 | }
19 |
--------------------------------------------------------------------------------
/docker/Dockerfile.user:
--------------------------------------------------------------------------------
1 | FROM node:20.12.0-alpine3.19
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY package.json package-lock.json turbo.json tsconfig.json ./
6 |
7 | COPY apps ./apps
8 | COPY packages ./packages
9 |
10 | # Install dependencies
11 | RUN npm install
12 | # Can you add a script to the global package.json that does this?
13 | RUN npm run db:generate
14 |
15 | # Can you filter the build down to just one app?
16 | RUN npm run build
17 |
18 | CMD ["npm", "run", "start-user-app"]
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "week-18",
3 | "private": true,
4 | "scripts": {
5 | "build": "turbo build",
6 | "dev": "turbo dev",
7 | "lint": "turbo lint",
8 | "format": "prettier --write \"**/*.{ts,tsx,md}\"",
9 | "db:generate": "cd packages/db && npx prisma generate && cd ../..",
10 | "start-user-app": "cd ./apps/user-app && npm run start"
11 |
12 | },
13 | "devDependencies": {
14 | "@repo/eslint-config": "*",
15 | "@repo/typescript-config": "*",
16 | "prettier": "^3.2.5",
17 | "turbo": "latest"
18 | },
19 | "engines": {
20 | "node": ">=18"
21 | },
22 | "packageManager": "npm@10.2.4",
23 | "workspaces": [
24 | "apps/*",
25 | "packages/*"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/packages/db/.env.example:
--------------------------------------------------------------------------------
1 |
2 | DATABASE_URL="postgresql://postgres:mysecretpassword@localhost:5432/postgres"
3 |
--------------------------------------------------------------------------------
/packages/db/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | # Keep environment variables out of version control
3 | .env
4 |
--------------------------------------------------------------------------------
/packages/db/index.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client'
2 |
3 | const prismaClientSingleton = () => {
4 | return new PrismaClient()
5 | }
6 |
7 | declare global {
8 | var prismaGlobal: undefined | ReturnType
9 | }
10 |
11 | const prisma: ReturnType = globalThis.prismaGlobal ?? prismaClientSingleton()
12 |
13 | export default prisma
14 |
15 | if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma
--------------------------------------------------------------------------------
/packages/db/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/db",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "@prisma/client": "^5.11.0",
6 | "bcrypt": "^5.1.1"
7 | },
8 | "devDependencies": {
9 | "prisma": "5.11.0"
10 | },
11 | "exports": {
12 | "./client": "./index.ts"
13 | },
14 | "prisma": {
15 | "seed": "ts-node prisma/seed.ts"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20240323121305_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" SERIAL NOT NULL,
4 | "email" TEXT NOT NULL,
5 | "name" TEXT,
6 |
7 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
8 | );
9 |
10 | -- CreateIndex
11 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
12 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20240324100733_add_merchant/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "AuthType" AS ENUM ('Google', 'Github');
3 |
4 | -- CreateTable
5 | CREATE TABLE "Merchant" (
6 | "id" SERIAL NOT NULL,
7 | "email" TEXT NOT NULL,
8 | "name" TEXT,
9 | "auth_type" "AuthType" NOT NULL,
10 |
11 | CONSTRAINT "Merchant_pkey" PRIMARY KEY ("id")
12 | );
13 |
14 | -- CreateIndex
15 | CREATE UNIQUE INDEX "Merchant_email_key" ON "Merchant"("email");
16 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20240324104524_add_merchant/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - Added the required column `number` to the `User` table without a default value. This is not possible if the table is not empty.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "User" ADD COLUMN "number" TEXT NOT NULL;
9 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20240324105137_add_password/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - A unique constraint covering the columns `[number]` on the table `User` will be added. If there are existing duplicate values, this will fail.
5 | - Added the required column `password` to the `User` table without a default value. This is not possible if the table is not empty.
6 |
7 | */
8 | -- AlterTable
9 | ALTER TABLE "User" ADD COLUMN "password" TEXT NOT NULL,
10 | ALTER COLUMN "email" DROP NOT NULL;
11 |
12 | -- CreateIndex
13 | CREATE UNIQUE INDEX "User_number_key" ON "User"("number");
14 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20240324145646_added_balances_and_onramp/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "OnRampStatus" AS ENUM ('Success', 'Failure', 'Processing');
3 |
4 | -- CreateTable
5 | CREATE TABLE "OnRampTransaction" (
6 | "id" SERIAL NOT NULL,
7 | "status" "OnRampStatus" NOT NULL,
8 | "token" TEXT NOT NULL,
9 | "provider" TEXT NOT NULL,
10 | "amount" INTEGER NOT NULL,
11 | "startTime" TIMESTAMP(3) NOT NULL,
12 | "userId" INTEGER NOT NULL,
13 |
14 | CONSTRAINT "OnRampTransaction_pkey" PRIMARY KEY ("id")
15 | );
16 |
17 | -- CreateTable
18 | CREATE TABLE "Balance" (
19 | "id" SERIAL NOT NULL,
20 | "userId" INTEGER NOT NULL,
21 | "amount" INTEGER NOT NULL,
22 | "locked" INTEGER NOT NULL,
23 |
24 | CONSTRAINT "Balance_pkey" PRIMARY KEY ("id")
25 | );
26 |
27 | -- CreateIndex
28 | CREATE UNIQUE INDEX "OnRampTransaction_token_key" ON "OnRampTransaction"("token");
29 |
30 | -- CreateIndex
31 | CREATE UNIQUE INDEX "Balance_userId_key" ON "Balance"("userId");
32 |
33 | -- AddForeignKey
34 | ALTER TABLE "OnRampTransaction" ADD CONSTRAINT "OnRampTransaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
35 |
36 | -- AddForeignKey
37 | ALTER TABLE "Balance" ADD CONSTRAINT "Balance_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
38 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20240330154923_adds_p2p_transfer/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "p2pTransfer" (
3 | "id" SERIAL NOT NULL,
4 | "amount" INTEGER NOT NULL,
5 | "timestamp" TIMESTAMP(3) NOT NULL,
6 | "fromUserId" INTEGER NOT NULL,
7 | "toUserId" INTEGER NOT NULL,
8 |
9 | CONSTRAINT "p2pTransfer_pkey" PRIMARY KEY ("id")
10 | );
11 |
12 | -- AddForeignKey
13 | ALTER TABLE "p2pTransfer" ADD CONSTRAINT "p2pTransfer_fromUserId_fkey" FOREIGN KEY ("fromUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
14 |
15 | -- AddForeignKey
16 | ALTER TABLE "p2pTransfer" ADD CONSTRAINT "p2pTransfer_toUserId_fkey" FOREIGN KEY ("toUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
17 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/packages/db/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "postgresql"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | model User {
11 | id Int @id @default(autoincrement())
12 | email String? @unique
13 | name String?
14 | number String @unique
15 | password String
16 | OnRampTransaction OnRampTransaction[]
17 | Balance Balance[]
18 | sentTransfers p2pTransfer[] @relation(name: "FromUserRelation")
19 | receivedTransfers p2pTransfer[] @relation(name: "ToUserRelation")
20 | }
21 |
22 | model Merchant {
23 | id Int @id @default(autoincrement())
24 | email String @unique
25 | name String?
26 | auth_type AuthType
27 | }
28 |
29 | model p2pTransfer {
30 | id Int @id @default(autoincrement())
31 | amount Int
32 | timestamp DateTime
33 | fromUserId Int
34 | fromUser User @relation(name: "FromUserRelation", fields: [fromUserId], references: [id])
35 | toUserId Int
36 | toUser User @relation(name: "ToUserRelation", fields: [toUserId], references: [id])
37 | }
38 |
39 | model OnRampTransaction {
40 | id Int @id @default(autoincrement())
41 | status OnRampStatus
42 | token String @unique
43 | provider String
44 | amount Int
45 | startTime DateTime
46 | userId Int
47 | user User @relation(fields: [userId], references: [id])
48 | }
49 |
50 | model Balance {
51 | id Int @id @default(autoincrement())
52 | userId Int @unique
53 | amount Int
54 | locked Int
55 | user User @relation(fields: [userId], references: [id])
56 | }
57 |
58 | enum AuthType {
59 | Google
60 | Github
61 | }
62 |
63 | enum OnRampStatus {
64 | Success
65 | Failure
66 | Processing
67 | }
68 |
--------------------------------------------------------------------------------
/packages/db/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client'
2 | import bcrypt from "bcrypt";
3 | const prisma = new PrismaClient()
4 |
5 | async function main() {
6 | const alice = await prisma.user.upsert({
7 | where: { number: '1111111111' },
8 | update: {},
9 | create: {
10 | number: '1111111111',
11 | password: await bcrypt.hash('alice', 10),
12 | name: 'alice',
13 | Balance: {
14 | create: {
15 | amount: 20000,
16 | locked: 0
17 | }
18 | },
19 | OnRampTransaction: {
20 | create: {
21 | startTime: new Date(),
22 | status: "Success",
23 | amount: 20000,
24 | token: "token__1",
25 | provider: "HDFC Bank",
26 | },
27 | },
28 | },
29 | })
30 | const bob = await prisma.user.upsert({
31 | where: { number: '2222222222' },
32 | update: {},
33 | create: {
34 | number: '2222222222',
35 | password: await bcrypt.hash('bob', 10),
36 | name: 'bob',
37 | Balance: {
38 | create: {
39 | amount: 2000,
40 | locked: 0
41 | }
42 | },
43 | OnRampTransaction: {
44 | create: {
45 | startTime: new Date(),
46 | status: "Failure",
47 | amount: 2000,
48 | token: "token__2",
49 | provider: "HDFC Bank",
50 | },
51 | },
52 | },
53 | })
54 | console.log({ alice, bob })
55 | }
56 | main()
57 | .then(async () => {
58 | await prisma.$disconnect()
59 | })
60 | .catch(async (e) => {
61 | console.error(e)
62 | await prisma.$disconnect()
63 | process.exit(1)
64 | })
--------------------------------------------------------------------------------
/packages/db/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/react-library.json",
3 | "compilerOptions": {
4 | "outDir": "dist"
5 | },
6 | "include": ["src"],
7 | "exclude": ["node_modules", "dist"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/eslint-config/README.md:
--------------------------------------------------------------------------------
1 | # `@turbo/eslint-config`
2 |
3 | Collection of internal eslint configurations.
4 |
--------------------------------------------------------------------------------
/packages/eslint-config/library.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /** @type {import("eslint").Linter.Config} */
6 | module.exports = {
7 | extends: ["eslint:recommended", "prettier", "eslint-config-turbo"],
8 | plugins: ["only-warn"],
9 | globals: {
10 | React: true,
11 | JSX: true,
12 | },
13 | env: {
14 | node: true,
15 | },
16 | settings: {
17 | "import/resolver": {
18 | typescript: {
19 | project,
20 | },
21 | },
22 | },
23 | ignorePatterns: [
24 | // Ignore dotfiles
25 | ".*.js",
26 | "node_modules/",
27 | "dist/",
28 | ],
29 | overrides: [
30 | {
31 | files: ["*.js?(x)", "*.ts?(x)"],
32 | },
33 | ],
34 | };
35 |
--------------------------------------------------------------------------------
/packages/eslint-config/next.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /** @type {import("eslint").Linter.Config} */
6 | module.exports = {
7 | extends: [
8 | "eslint:recommended",
9 | "prettier",
10 | require.resolve("@vercel/style-guide/eslint/next"),
11 | "eslint-config-turbo",
12 | ],
13 | globals: {
14 | React: true,
15 | JSX: true,
16 | },
17 | env: {
18 | node: true,
19 | browser: true,
20 | },
21 | plugins: ["only-warn"],
22 | settings: {
23 | "import/resolver": {
24 | typescript: {
25 | project,
26 | },
27 | },
28 | },
29 | ignorePatterns: [
30 | // Ignore dotfiles
31 | ".*.js",
32 | "node_modules/",
33 | ],
34 | overrides: [{ files: ["*.js?(x)", "*.ts?(x)"] }],
35 | };
36 |
--------------------------------------------------------------------------------
/packages/eslint-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/eslint-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "files": [
6 | "library.js",
7 | "next.js",
8 | "react-internal.js"
9 | ],
10 | "devDependencies": {
11 | "@vercel/style-guide": "^5.2.0",
12 | "eslint-config-turbo": "^1.12.4",
13 | "eslint-config-prettier": "^9.1.0",
14 | "eslint-plugin-only-warn": "^1.1.0",
15 | "@typescript-eslint/parser": "^7.1.0",
16 | "@typescript-eslint/eslint-plugin": "^7.1.0",
17 | "typescript": "^5.3.3"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/eslint-config/react-internal.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /*
6 | * This is a custom ESLint configuration for use with
7 | * internal (bundled by their consumer) libraries
8 | * that utilize React.
9 | *
10 | * This config extends the Vercel Engineering Style Guide.
11 | * For more information, see https://github.com/vercel/style-guide
12 | *
13 | */
14 |
15 | /** @type {import("eslint").Linter.Config} */
16 | module.exports = {
17 | extends: ["eslint:recommended", "prettier", "eslint-config-turbo"],
18 | plugins: ["only-warn"],
19 | globals: {
20 | React: true,
21 | JSX: true,
22 | },
23 | env: {
24 | browser: true,
25 | },
26 | settings: {
27 | "import/resolver": {
28 | typescript: {
29 | project,
30 | },
31 | },
32 | },
33 | ignorePatterns: [
34 | // Ignore dotfiles
35 | ".*.js",
36 | "node_modules/",
37 | "dist/",
38 | ],
39 | overrides: [
40 | // Force ESLint to detect .tsx files
41 | { files: ["*.js?(x)", "*.ts?(x)"] },
42 | ],
43 | };
44 |
--------------------------------------------------------------------------------
/packages/store/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/store",
3 | "version": "1.0.0",
4 | "private": true,
5 | "dependencies": {
6 | "recoil": "^0.7.7"
7 | },
8 | "exports": {
9 | "./balance": "./src/hooks/useBalance.ts"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/store/src/atoms/balance.ts:
--------------------------------------------------------------------------------
1 |
2 | import { atom } from "recoil";
3 |
4 | export const balanceAtom = atom({
5 | key: "balance",
6 | default: 0,
7 | })
--------------------------------------------------------------------------------
/packages/store/src/hooks/useBalance.ts:
--------------------------------------------------------------------------------
1 | import { useRecoilValue } from "recoil"
2 | import { balanceAtom } from "../atoms/balance"
3 |
4 | export const useBalance = () => {
5 | const value = useRecoilValue(balanceAtom);
6 | return value;
7 | }
--------------------------------------------------------------------------------
/packages/store/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/react-library.json",
3 | "compilerOptions": {
4 | "outDir": "dist"
5 | },
6 | "include": ["src"],
7 | "exclude": ["node_modules", "dist"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/typescript-config/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "declaration": true,
6 | "declarationMap": true,
7 | "esModuleInterop": true,
8 | "incremental": false,
9 | "isolatedModules": true,
10 | "lib": ["es2022", "DOM", "DOM.Iterable"],
11 | "module": "NodeNext",
12 | "moduleDetection": "force",
13 | "moduleResolution": "NodeNext",
14 | "noUncheckedIndexedAccess": true,
15 | "resolveJsonModule": true,
16 | "skipLibCheck": true,
17 | "strict": true,
18 | "target": "ES2022"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/typescript-config/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Next.js",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "plugins": [{ "name": "next" }],
7 | "module": "ESNext",
8 | "moduleResolution": "Bundler",
9 | "allowJs": true,
10 | "jsx": "preserve",
11 | "noEmit": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/typescript-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/typescript-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "publishConfig": {
7 | "access": "public"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/typescript-config/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "React Library",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "jsx": "react-jsx"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/ui/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /** @type {import("eslint").Linter.Config} */
2 | module.exports = {
3 | root: true,
4 | extends: ["@repo/eslint-config/react-internal.js"],
5 | parser: "@typescript-eslint/parser",
6 | parserOptions: {
7 | project: "./tsconfig.lint.json",
8 | tsconfigRootDir: __dirname,
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/packages/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/ui",
3 | "version": "0.0.0",
4 | "private": true,
5 | "exports": {
6 | "./button": "./src/button.tsx",
7 | "./card": "./src/card.tsx",
8 | "./code": "./src/code.tsx",
9 | "./appbar": "./src/Appbar.tsx",
10 | "./center": "./src/Center.tsx",
11 | "./select": "./src/Select.tsx",
12 | "./textinput": "./src/TextInput.tsx"
13 | },
14 | "scripts": {
15 | "lint": "eslint . --max-warnings 0",
16 | "generate:component": "turbo gen react-component"
17 | },
18 | "devDependencies": {
19 | "@repo/eslint-config": "*",
20 | "@repo/typescript-config": "*",
21 | "@turbo/gen": "^1.12.4",
22 | "@types/node": "^20.11.24",
23 | "@types/eslint": "^8.56.5",
24 | "@types/react": "^18.2.61",
25 | "@types/react-dom": "^18.2.19",
26 | "eslint": "^8.57.0",
27 | "react": "^18.2.0",
28 | "typescript": "^5.3.3"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/ui/src/Appbar.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "./button";
2 |
3 | interface AppbarProps {
4 | user?: {
5 | name?: string | null;
6 | },
7 | // TODO: can u figure out what the type should be here?
8 | onSignin: any,
9 | onSignout: any
10 | }
11 |
12 | export const Appbar = ({
13 | user,
14 | onSignin,
15 | onSignout
16 | }: AppbarProps) => {
17 | return
18 |
19 | PayTM
20 |
21 |
22 |
23 |
24 |
25 | }
--------------------------------------------------------------------------------
/packages/ui/src/Center.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | export const Center = ({ children }: { children: React.ReactNode }) => {
4 | return
5 |
6 | {children}
7 |
8 |
9 | }
--------------------------------------------------------------------------------
/packages/ui/src/Select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | export const Select = ({ options, onSelect }: {
3 | onSelect: (value: string) => void;
4 | options: {
5 | key: string;
6 | value: string;
7 | }[];
8 | }) => {
9 | return
14 | }
--------------------------------------------------------------------------------
/packages/ui/src/TextInput.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | export const TextInput = ({
4 | placeholder,
5 | onChange,
6 | label
7 | }: {
8 | placeholder: string;
9 | onChange: (value: string) => void;
10 | label: string;
11 | }) => {
12 | return
13 |
14 | onChange(e.target.value)} type="text" id="first_name" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" placeholder={placeholder} />
15 |
16 | }
--------------------------------------------------------------------------------
/packages/ui/src/button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ReactNode } from "react";
4 |
5 | interface ButtonProps {
6 | children: ReactNode;
7 | onClick: () => void;
8 | }
9 |
10 | export const Button = ({ onClick, children }: ButtonProps) => {
11 | return (
12 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/packages/ui/src/card.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export function Card({
4 | title,
5 | children,
6 | }: {
7 | title: string;
8 | children?: React.ReactNode;
9 | }): JSX.Element {
10 | return (
11 |
14 |
15 | {title}
16 |
17 |
{children}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/packages/ui/src/code.tsx:
--------------------------------------------------------------------------------
1 | export function Code({
2 | children,
3 | className,
4 | }: {
5 | children: React.ReactNode;
6 | className?: string;
7 | }): JSX.Element {
8 | return {children}
;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/react-library.json",
3 | "compilerOptions": {
4 | "outDir": "dist"
5 | },
6 | "include": ["src"],
7 | "exclude": ["node_modules", "dist"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/ui/tsconfig.lint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/react-library.json",
3 | "compilerOptions": {
4 | "outDir": "dist"
5 | },
6 | "include": ["src", "turbo"],
7 | "exclude": ["node_modules", "dist"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/ui/turbo/generators/config.ts:
--------------------------------------------------------------------------------
1 | import type { PlopTypes } from "@turbo/gen";
2 |
3 | // Learn more about Turborepo Generators at https://turbo.build/repo/docs/core-concepts/monorepos/code-generation
4 |
5 | export default function generator(plop: PlopTypes.NodePlopAPI): void {
6 | // A simple generator to add a new React component to the internal UI library
7 | plop.setGenerator("react-component", {
8 | description: "Adds a new react component",
9 | prompts: [
10 | {
11 | type: "input",
12 | name: "name",
13 | message: "What is the name of the component?",
14 | },
15 | ],
16 | actions: [
17 | {
18 | type: "add",
19 | path: "src/{{kebabCase name}}.tsx",
20 | templateFile: "templates/component.hbs",
21 | },
22 | {
23 | type: "append",
24 | path: "package.json",
25 | pattern: /"exports": {(?)/g,
26 | template: '"./{{kebabCase name}}": "./src/{{kebabCase name}}.tsx",',
27 | },
28 | ],
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/packages/ui/turbo/generators/templates/component.hbs:
--------------------------------------------------------------------------------
1 | export const {{ pascalCase name }} = ({ children }: { children: React.ReactNode }) => {
2 | return (
3 |
4 |
{{ pascalCase name }} Component
5 | {children}
6 |
7 | );
8 | };
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/base.json"
3 | }
4 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "globalDependencies": ["**/.env.*local"],
4 | "pipeline": {
5 | "build": {
6 | "dependsOn": ["^build"],
7 | "outputs": [".next/**", "!.next/cache/**"]
8 | },
9 | "lint": {
10 | "dependsOn": ["^lint"]
11 | },
12 | "dev": {
13 | "cache": false,
14 | "persistent": true
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------