├── .gitignore ├── .npmrc ├── README.md ├── apps ├── api │ ├── .gitignore │ ├── README.md │ ├── config.ts │ ├── index.ts │ ├── middleware.ts │ ├── package.json │ ├── tsconfig.json │ └── types.d.ts ├── frontend │ ├── .gitignore │ ├── README.md │ ├── app │ │ ├── dashboard │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── components.json │ ├── components │ │ ├── Appbar.tsx │ │ ├── theme-provider.tsx │ │ └── ui │ │ │ └── button.tsx │ ├── config.ts │ ├── eslint.config.mjs │ ├── hooks │ │ └── useWebsites.tsx │ ├── lib │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ │ ├── file.svg │ │ ├── globe.svg │ │ ├── next.svg │ │ ├── vercel.svg │ │ └── window.svg │ └── tsconfig.json ├── hub │ ├── .gitignore │ ├── README.md │ ├── index.ts │ ├── package.json │ └── tsconfig.json └── validator │ ├── .gitignore │ ├── README.md │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── bun.lock ├── package-lock.json ├── package.json ├── packages ├── common │ ├── .gitignore │ ├── README.md │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── db │ ├── .gitignore │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── prisma │ │ ├── migrations │ │ │ ├── 20250314011543_init │ │ │ │ └── migration.sql │ │ │ ├── 20250314012859_added_disabled │ │ │ │ └── migration.sql │ │ │ ├── 20250315042613_added_payout │ │ │ │ └── migration.sql │ │ │ ├── 20250315042714_fix_payout │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ ├── schema.prisma │ │ └── seed.ts │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── eslint-config │ ├── README.md │ ├── base.js │ ├── next.js │ ├── package.json │ └── react-internal.js ├── typescript-config │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ui │ ├── eslint.config.mjs │ ├── package.json │ ├── src │ ├── button.tsx │ ├── card.tsx │ └── code.tsx │ ├── tsconfig.json │ └── turbo │ └── generators │ ├── config.ts │ └── templates │ └── component.hbs └── turbo.json /.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/code100x/dpin-uptime/fc49183c2aef51e7dd871f98c943b62b4ac6a02c/.npmrc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Turborepo starter 2 | 3 | This Turborepo starter is maintained by the Turborepo core team. 4 | 5 | ## Using this example 6 | 7 | Run the following command: 8 | 9 | ```sh 10 | npx create-turbo@latest 11 | ``` 12 | 13 | ## What's inside? 14 | 15 | This Turborepo includes the following packages/apps: 16 | 17 | ### Apps and Packages 18 | 19 | - `docs`: a [Next.js](https://nextjs.org/) app 20 | - `web`: another [Next.js](https://nextjs.org/) app 21 | - `@repo/ui`: a stub React component library shared by both `web` and `docs` applications 22 | - `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`) 23 | - `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo 24 | 25 | Each package/app is 100% [TypeScript](https://www.typescriptlang.org/). 26 | 27 | ### Utilities 28 | 29 | This Turborepo has some additional tools already setup for you: 30 | 31 | - [TypeScript](https://www.typescriptlang.org/) for static type checking 32 | - [ESLint](https://eslint.org/) for code linting 33 | - [Prettier](https://prettier.io) for code formatting 34 | 35 | ### Build 36 | 37 | To build all apps and packages, run the following command: 38 | 39 | ``` 40 | cd my-turborepo 41 | pnpm build 42 | ``` 43 | 44 | ### Develop 45 | 46 | To develop all apps and packages, run the following command: 47 | 48 | ``` 49 | cd my-turborepo 50 | pnpm dev 51 | ``` 52 | 53 | ### Remote Caching 54 | 55 | > [!TIP] 56 | > Vercel Remote Cache is free for all plans. Get started today at [vercel.com](https://vercel.com/signup?/signup?utm_source=remote-cache-sdk&utm_campaign=free_remote_cache). 57 | 58 | Turborepo can use a technique known as [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines. 59 | 60 | By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup?utm_source=turborepo-examples), then enter the following commands: 61 | 62 | ``` 63 | cd my-turborepo 64 | npx turbo login 65 | ``` 66 | 67 | This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview). 68 | 69 | Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo: 70 | 71 | ``` 72 | npx turbo link 73 | ``` 74 | 75 | ## Useful Links 76 | 77 | Learn more about the power of Turborepo: 78 | 79 | - [Tasks](https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks) 80 | - [Caching](https://turbo.build/repo/docs/core-concepts/caching) 81 | - [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching) 82 | - [Filtering](https://turbo.build/repo/docs/core-concepts/monorepos/filtering) 83 | - [Configuration Options](https://turbo.build/repo/docs/reference/configuration) 84 | - [CLI Usage](https://turbo.build/repo/docs/reference/command-line-reference) 85 | -------------------------------------------------------------------------------- /apps/api/.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 | -------------------------------------------------------------------------------- /apps/api/README.md: -------------------------------------------------------------------------------- 1 | # api 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.2.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /apps/api/config.ts: -------------------------------------------------------------------------------- 1 | 2 | export const JWT_PUBLIC_KEY = process.env.JWT_PUBLIC_KEY || `-----BEGIN PUBLIC KEY----- 3 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxYRjjCo+ZhdDrsVduRaj 4 | o4U9XNYbUxG4LBgtrHUpvMrRda6yXXfuecC0zwdF4O16ZNp9LMyy2bllXG8HOWyw 5 | ac/r/9Zt98A9jMUEKq/AhRvaRaavr9jMFceJNTebFMOnzPRZGIhMWWOGPsrwqWLa 6 | JbMtnAd2IlQc4MNJmns2NeaPmujDysZDmN5BwLd0LzTaktOFsrVCFYu7MiL7sYi9 7 | 229OovGxu55Y1qpekwt42nuipbpZEx59FdAIpJCgjsp8GeqavC5ySWuhRbKGTO+e 8 | qiBV0S9m134FuGRc67HEYcr7BkTSFwF4pD/L2culHEBaRedilGdRbNAGFAQhN4mS 9 | yQIDAQAB 10 | -----END PUBLIC KEY----- 11 | `; -------------------------------------------------------------------------------- /apps/api/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express" 2 | import { authMiddleware } from "./middleware"; 3 | import { prismaClient } from "db/client"; 4 | import cors from "cors"; 5 | import { Transaction, SystemProgram, Connection } from "@solana/web3.js"; 6 | 7 | 8 | const connection = new Connection("https://api.mainnet-beta.solana.com"); 9 | const app = express(); 10 | 11 | app.use(cors()); 12 | app.use(express.json()); 13 | 14 | app.post("/api/v1/website", authMiddleware, async (req, res) => { 15 | const userId = req.userId!; 16 | const { url } = req.body; 17 | 18 | const data = await prismaClient.website.create({ 19 | data: { 20 | userId, 21 | url 22 | } 23 | }) 24 | 25 | res.json({ 26 | id: data.id 27 | }) 28 | }) 29 | 30 | app.get("/api/v1/website/status", authMiddleware, async (req, res) => { 31 | const websiteId = req.query.websiteId! as unknown as string; 32 | const userId = req.userId; 33 | 34 | const data = await prismaClient.website.findFirst({ 35 | where: { 36 | id: websiteId, 37 | userId, 38 | disabled: false 39 | }, 40 | include: { 41 | ticks: true 42 | } 43 | }) 44 | 45 | res.json(data) 46 | 47 | }) 48 | 49 | app.get("/api/v1/websites", authMiddleware, async (req, res) => { 50 | const userId = req.userId!; 51 | 52 | const websites = await prismaClient.website.findMany({ 53 | where: { 54 | userId, 55 | disabled: false 56 | }, 57 | include: { 58 | ticks: true 59 | } 60 | }) 61 | 62 | res.json({ 63 | websites 64 | }) 65 | }) 66 | 67 | app.delete("/api/v1/website/", authMiddleware, async (req, res) => { 68 | const websiteId = req.body.websiteId; 69 | const userId = req.userId!; 70 | 71 | await prismaClient.website.update({ 72 | where: { 73 | id: websiteId, 74 | userId 75 | }, 76 | data: { 77 | disabled: true 78 | } 79 | }) 80 | 81 | res.json({ 82 | message: "Deleted website successfully" 83 | }) 84 | }) 85 | 86 | app.post("/api/v1/payout/:validatorId", async (req, res) => { 87 | 88 | }) 89 | 90 | app.listen(8080); 91 | -------------------------------------------------------------------------------- /apps/api/middleware.ts: -------------------------------------------------------------------------------- 1 | import type { NextFunction, Request, Response } from "express"; 2 | import jwt from "jsonwebtoken"; 3 | import { JWT_PUBLIC_KEY } from "./config"; 4 | 5 | export function authMiddleware(req: Request, res: Response, next: NextFunction) { 6 | const token = req.headers['authorization']; 7 | if (!token) { 8 | return res.status(401).json({ error: 'Unauthorized' }); 9 | } 10 | 11 | const decoded = jwt.verify(token, JWT_PUBLIC_KEY); 12 | console.log(decoded); 13 | if (!decoded || !decoded.sub) { 14 | return res.status(401).json({ error: 'Unauthorized' }); 15 | } 16 | 17 | req.userId = decoded.sub as string; 18 | 19 | next() 20 | } -------------------------------------------------------------------------------- /apps/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 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 | "@solana/web3.js": "^1.98.0", 13 | "@types/cors": "^2.8.17", 14 | "@types/express": "^5.0.0", 15 | "@types/jsonwebtoken": "^9.0.9", 16 | "cors": "^2.8.5", 17 | "db": "*", 18 | "express": "^4.21.2", 19 | "jsonwebtoken": "^9.0.2", 20 | "jwt": "^0.2.0" 21 | } 22 | } -------------------------------------------------------------------------------- /apps/api/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 | -------------------------------------------------------------------------------- /apps/api/types.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare namespace Express { 3 | interface Request { 4 | userId?: string 5 | } 6 | } -------------------------------------------------------------------------------- /apps/frontend/.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /apps/frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /apps/frontend/app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState, useMemo } from 'react'; 3 | import { ChevronDown, ChevronUp, Globe, Plus, Moon, Sun } from 'lucide-react'; 4 | import { useWebsites } from '@/hooks/useWebsites'; 5 | import axios from 'axios'; 6 | import { API_BACKEND_URL } from '@/config'; 7 | import { useAuth } from '@clerk/nextjs'; 8 | 9 | type UptimeStatus = "good" | "bad" | "unknown"; 10 | 11 | function StatusCircle({ status }: { status: UptimeStatus }) { 12 | return ( 13 |
14 | ); 15 | } 16 | 17 | function UptimeTicks({ ticks }: { ticks: UptimeStatus[] }) { 18 | return ( 19 |
20 | {ticks.map((tick, index) => ( 21 |
27 | ))} 28 |
29 | ); 30 | } 31 | 32 | function CreateWebsiteModal({ isOpen, onClose }: { isOpen: boolean; onClose: (url: string | null) => void }) { 33 | const [url, setUrl] = useState(''); 34 | if (!isOpen) return null; 35 | 36 | return ( 37 |
38 |
39 |

Add New Website

40 |
41 | 44 | setUrl(e.target.value)} 50 | /> 51 |
52 |
53 | 60 | 67 |
68 |
69 |
70 | ); 71 | } 72 | 73 | interface ProcessedWebsite { 74 | id: string; 75 | url: string; 76 | status: UptimeStatus; 77 | uptimePercentage: number; 78 | lastChecked: string; 79 | uptimeTicks: UptimeStatus[]; 80 | } 81 | 82 | function WebsiteCard({ website }: { website: ProcessedWebsite }) { 83 | const [isExpanded, setIsExpanded] = useState(false); 84 | 85 | return ( 86 |
87 |
setIsExpanded(!isExpanded)} 90 | > 91 |
92 | 93 |
94 |

{website.url}

95 |
96 |
97 |
98 | 99 | {website.uptimePercentage.toFixed(1)}% uptime 100 | 101 | {isExpanded ? ( 102 | 103 | ) : ( 104 | 105 | )} 106 |
107 |
108 | 109 | {isExpanded && ( 110 |
111 |
112 |

Last 30 minutes status:

113 | 114 |
115 |

116 | Last checked: {website.lastChecked} 117 |

118 |
119 | )} 120 |
121 | ); 122 | } 123 | 124 | function App() { 125 | const [isDarkMode, setIsDarkMode] = useState(false); 126 | const [isModalOpen, setIsModalOpen] = useState(false); 127 | const {websites, refreshWebsites} = useWebsites(); 128 | const { getToken } = useAuth(); 129 | 130 | const processedWebsites = useMemo(() => { 131 | return websites.map(website => { 132 | // Sort ticks by creation time 133 | const sortedTicks = [...website.ticks].sort((a, b) => 134 | new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() 135 | ); 136 | 137 | // Get the most recent 30 minutes of ticks 138 | const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000); 139 | const recentTicks = sortedTicks.filter(tick => 140 | new Date(tick.createdAt) > thirtyMinutesAgo 141 | ); 142 | 143 | // Aggregate ticks into 3-minute windows (10 windows total) 144 | const windows: UptimeStatus[] = []; 145 | 146 | for (let i = 0; i < 10; i++) { 147 | const windowStart = new Date(Date.now() - (i + 1) * 3 * 60 * 1000); 148 | const windowEnd = new Date(Date.now() - i * 3 * 60 * 1000); 149 | 150 | const windowTicks = recentTicks.filter(tick => { 151 | const tickTime = new Date(tick.createdAt); 152 | return tickTime >= windowStart && tickTime < windowEnd; 153 | }); 154 | 155 | // Window is considered up if majority of ticks are up 156 | const upTicks = windowTicks.filter(tick => tick.status === 'Good').length; 157 | windows[9 - i] = windowTicks.length === 0 ? "unknown" : (upTicks / windowTicks.length) >= 0.5 ? "good" : "bad"; 158 | } 159 | 160 | // Calculate overall status and uptime percentage 161 | const totalTicks = sortedTicks.length; 162 | const upTicks = sortedTicks.filter(tick => tick.status === 'Good').length; 163 | const uptimePercentage = totalTicks === 0 ? 100 : (upTicks / totalTicks) * 100; 164 | 165 | // Get the most recent status 166 | const currentStatus = windows[windows.length - 1]; 167 | 168 | // Format the last checked time 169 | const lastChecked = sortedTicks[0] 170 | ? new Date(sortedTicks[0].createdAt).toLocaleTimeString() 171 | : 'Never'; 172 | 173 | return { 174 | id: website.id, 175 | url: website.url, 176 | status: currentStatus, 177 | uptimePercentage, 178 | lastChecked, 179 | uptimeTicks: windows, 180 | }; 181 | }); 182 | }, [websites]); 183 | 184 | // Toggle dark mode 185 | React.useEffect(() => { 186 | if (isDarkMode) { 187 | document.documentElement.classList.add('dark'); 188 | } else { 189 | document.documentElement.classList.remove('dark'); 190 | } 191 | }, [isDarkMode]); 192 | 193 | return ( 194 |
195 |
196 |
197 |
198 | 199 |

Uptime Monitor

200 |
201 |
202 | 212 | 219 |
220 |
221 | 222 |
223 | {processedWebsites.map((website) => ( 224 | 225 | ))} 226 |
227 |
228 | 229 | { 232 | if (url === null) { 233 | setIsModalOpen(false); 234 | return; 235 | } 236 | 237 | const token = await getToken(); 238 | setIsModalOpen(false) 239 | axios.post(`${API_BACKEND_URL}/api/v1/website`, { 240 | url, 241 | }, { 242 | headers: { 243 | Authorization: token, 244 | }, 245 | }) 246 | .then(() => { 247 | refreshWebsites(); 248 | }) 249 | }} 250 | /> 251 |
252 | ); 253 | } 254 | 255 | export default App; -------------------------------------------------------------------------------- /apps/frontend/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code100x/dpin-uptime/fc49183c2aef51e7dd871f98c943b62b4ac6a02c/apps/frontend/app/favicon.ico -------------------------------------------------------------------------------- /apps/frontend/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @plugin "tailwindcss-animate"; 4 | 5 | @custom-variant dark (&:is(.dark *)); 6 | 7 | @theme inline { 8 | --color-background: var(--background); 9 | --color-foreground: var(--foreground); 10 | --font-sans: var(--font-geist-sans); 11 | --font-mono: var(--font-geist-mono); 12 | --color-sidebar-ring: var(--sidebar-ring); 13 | --color-sidebar-border: var(--sidebar-border); 14 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 15 | --color-sidebar-accent: var(--sidebar-accent); 16 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 17 | --color-sidebar-primary: var(--sidebar-primary); 18 | --color-sidebar-foreground: var(--sidebar-foreground); 19 | --color-sidebar: var(--sidebar); 20 | --color-chart-5: var(--chart-5); 21 | --color-chart-4: var(--chart-4); 22 | --color-chart-3: var(--chart-3); 23 | --color-chart-2: var(--chart-2); 24 | --color-chart-1: var(--chart-1); 25 | --color-ring: var(--ring); 26 | --color-input: var(--input); 27 | --color-border: var(--border); 28 | --color-destructive: var(--destructive); 29 | --color-accent-foreground: var(--accent-foreground); 30 | --color-accent: var(--accent); 31 | --color-muted-foreground: var(--muted-foreground); 32 | --color-muted: var(--muted); 33 | --color-secondary-foreground: var(--secondary-foreground); 34 | --color-secondary: var(--secondary); 35 | --color-primary-foreground: var(--primary-foreground); 36 | --color-primary: var(--primary); 37 | --color-popover-foreground: var(--popover-foreground); 38 | --color-popover: var(--popover); 39 | --color-card-foreground: var(--card-foreground); 40 | --color-card: var(--card); 41 | --radius-sm: calc(var(--radius) - 4px); 42 | --radius-md: calc(var(--radius) - 2px); 43 | --radius-lg: var(--radius); 44 | --radius-xl: calc(var(--radius) + 4px); 45 | } 46 | 47 | :root { 48 | --radius: 0.625rem; 49 | --background: oklch(1 0 0); 50 | --foreground: oklch(0.145 0 0); 51 | --card: oklch(1 0 0); 52 | --card-foreground: oklch(0.145 0 0); 53 | --popover: oklch(1 0 0); 54 | --popover-foreground: oklch(0.145 0 0); 55 | --primary: oklch(0.205 0 0); 56 | --primary-foreground: oklch(0.985 0 0); 57 | --secondary: oklch(0.97 0 0); 58 | --secondary-foreground: oklch(0.205 0 0); 59 | --muted: oklch(0.97 0 0); 60 | --muted-foreground: oklch(0.556 0 0); 61 | --accent: oklch(0.97 0 0); 62 | --accent-foreground: oklch(0.205 0 0); 63 | --destructive: oklch(0.577 0.245 27.325); 64 | --border: oklch(0.922 0 0); 65 | --input: oklch(0.922 0 0); 66 | --ring: oklch(0.708 0 0); 67 | --chart-1: oklch(0.646 0.222 41.116); 68 | --chart-2: oklch(0.6 0.118 184.704); 69 | --chart-3: oklch(0.398 0.07 227.392); 70 | --chart-4: oklch(0.828 0.189 84.429); 71 | --chart-5: oklch(0.769 0.188 70.08); 72 | --sidebar: oklch(0.985 0 0); 73 | --sidebar-foreground: oklch(0.145 0 0); 74 | --sidebar-primary: oklch(0.205 0 0); 75 | --sidebar-primary-foreground: oklch(0.985 0 0); 76 | --sidebar-accent: oklch(0.97 0 0); 77 | --sidebar-accent-foreground: oklch(0.205 0 0); 78 | --sidebar-border: oklch(0.922 0 0); 79 | --sidebar-ring: oklch(0.708 0 0); 80 | } 81 | 82 | .dark { 83 | --background: oklch(0.145 0 0); 84 | --foreground: oklch(0.985 0 0); 85 | --card: oklch(0.205 0 0); 86 | --card-foreground: oklch(0.985 0 0); 87 | --popover: oklch(0.205 0 0); 88 | --popover-foreground: oklch(0.985 0 0); 89 | --primary: oklch(0.922 0 0); 90 | --primary-foreground: oklch(0.205 0 0); 91 | --secondary: oklch(0.269 0 0); 92 | --secondary-foreground: oklch(0.985 0 0); 93 | --muted: oklch(0.269 0 0); 94 | --muted-foreground: oklch(0.708 0 0); 95 | --accent: oklch(0.269 0 0); 96 | --accent-foreground: oklch(0.985 0 0); 97 | --destructive: oklch(0.704 0.191 22.216); 98 | --border: oklch(1 0 0 / 10%); 99 | --input: oklch(1 0 0 / 15%); 100 | --ring: oklch(0.556 0 0); 101 | --chart-1: oklch(0.488 0.243 264.376); 102 | --chart-2: oklch(0.696 0.17 162.48); 103 | --chart-3: oklch(0.769 0.188 70.08); 104 | --chart-4: oklch(0.627 0.265 303.9); 105 | --chart-5: oklch(0.645 0.246 16.439); 106 | --sidebar: oklch(0.205 0 0); 107 | --sidebar-foreground: oklch(0.985 0 0); 108 | --sidebar-primary: oklch(0.488 0.243 264.376); 109 | --sidebar-primary-foreground: oklch(0.985 0 0); 110 | --sidebar-accent: oklch(0.269 0 0); 111 | --sidebar-accent-foreground: oklch(0.985 0 0); 112 | --sidebar-border: oklch(1 0 0 / 10%); 113 | --sidebar-ring: oklch(0.556 0 0); 114 | } 115 | 116 | @layer base { 117 | * { 118 | @apply border-border outline-ring/50; 119 | } 120 | body { 121 | @apply bg-background text-foreground; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /apps/frontend/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | import { 5 | ClerkProvider 6 | } from '@clerk/nextjs' 7 | import { Appbar } from "../components/Appbar"; 8 | import { ThemeProvider } from "@/components/theme-provider"; 9 | 10 | const geistSans = Geist({ 11 | variable: "--font-geist-sans", 12 | subsets: ["latin"], 13 | }); 14 | 15 | const geistMono = Geist_Mono({ 16 | variable: "--font-geist-mono", 17 | subsets: ["latin"], 18 | }); 19 | 20 | export const metadata: Metadata = { 21 | title: "Create Next App", 22 | description: "Generated by create next app", 23 | }; 24 | 25 | export default function RootLayout({ 26 | children, 27 | }: Readonly<{ 28 | children: React.ReactNode; 29 | }>) { 30 | return ( 31 | 32 | 33 | 36 | 37 | 38 | {children} 39 | 40 | 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /apps/frontend/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import React, { useEffect, useState } from 'react'; 3 | import { Activity, Bell, Clock, Server, ArrowRight, Check, Moon, Sun } from 'lucide-react'; 4 | import { useRouter } from 'next/navigation'; 5 | 6 | function App() { 7 | const [darkMode, setDarkMode] = useState(false); 8 | const router = useRouter(); 9 | 10 | useEffect(() => { 11 | if (darkMode) { 12 | document.documentElement.classList.add('dark'); 13 | } else { 14 | document.documentElement.classList.remove('dark'); 15 | } 16 | }, [darkMode]); 17 | 18 | return ( 19 |
20 | {/* Hero Section */} 21 |
22 |
23 |
24 |

25 | Monitor Your Services with Confidence 26 |

27 |

28 | Get instant alerts when your services go down. Monitor uptime, performance, and ensure your business never misses a beat. 29 |

30 |
31 | 35 | 38 |
39 |
40 |
41 | Dashboard 46 |
47 |
48 |
49 | 50 | {/* Features */} 51 |
52 |
53 |

54 | Everything you need for reliable monitoring 55 |

56 |
57 | } 59 | title="Instant Alerts" 60 | description="Get notified immediately when your services experience downtime through multiple channels." 61 | /> 62 | } 64 | title="24/7 Monitoring" 65 | description="Round-the-clock monitoring from multiple locations worldwide." 66 | /> 67 | } 69 | title="Detailed Reports" 70 | description="Comprehensive reports and analytics to track your service performance." 71 | /> 72 |
73 |
74 |
75 | 76 | {/* Pricing */} 77 |
78 |
79 |

80 | Simple, transparent pricing 81 |

82 |
83 | 94 | 107 | 119 |
120 |
121 |
122 | 123 | {/* Footer */} 124 |
125 |
126 |
127 |
128 |
129 | 130 | UptimeGuard 131 |
132 |

133 | Keeping your services online, always. 134 |

135 |
136 |
137 |

Product

138 | 143 |
144 |
145 |

Company

146 | 151 |
152 |
153 |

Legal

154 | 159 |
160 |
161 |
162 |

© 2025 UptimeGuard. All rights reserved.

163 |
164 |
165 |
166 |
167 | ); 168 | } 169 | 170 | function FeatureCard({ icon, title, description }) { 171 | return ( 172 |
173 |
{icon}
174 |

{title}

175 |

{description}

176 |
177 | ); 178 | } 179 | 180 | function PricingCard({ title, price, features, featured = false }) { 181 | return ( 182 |
187 |

{title}

188 |
189 | ${price} 190 | /month 191 |
192 |
    193 | {features.map((feature, index) => ( 194 |
  • 195 | 196 | {feature} 197 |
  • 198 | ))} 199 |
200 | 209 |
210 | ); 211 | } 212 | 213 | export default App; -------------------------------------------------------------------------------- /apps/frontend/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": "", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 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 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /apps/frontend/components/Appbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | SignInButton, 5 | SignUpButton, 6 | SignedIn, 7 | SignedOut, 8 | UserButton, 9 | } from '@clerk/nextjs' 10 | 11 | export function Appbar() { 12 | return
13 |
DPin Uptime
14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 | } -------------------------------------------------------------------------------- /apps/frontend/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ThemeProvider as NextThemesProvider } from "next-themes" 5 | 6 | export function ThemeProvider({ 7 | children, 8 | ...props 9 | }: React.ComponentProps) { 10 | return {children} 11 | } 12 | -------------------------------------------------------------------------------- /apps/frontend/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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ) 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean 47 | }) { 48 | const Comp = asChild ? Slot : "button" 49 | 50 | return ( 51 | 56 | ) 57 | } 58 | 59 | export { Button, buttonVariants } 60 | -------------------------------------------------------------------------------- /apps/frontend/config.ts: -------------------------------------------------------------------------------- 1 | export const API_BACKEND_URL = "http://localhost:8080"; -------------------------------------------------------------------------------- /apps/frontend/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /apps/frontend/hooks/useWebsites.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { API_BACKEND_URL } from "@/config"; 3 | import { useAuth } from "@clerk/nextjs"; 4 | import axios from "axios"; 5 | import { useEffect, useState } from "react"; 6 | 7 | interface Website { 8 | id: string; 9 | url: string; 10 | ticks: { 11 | id: string; 12 | createdAt: string; 13 | status: string; 14 | latency: number; 15 | }[]; 16 | } 17 | 18 | export function useWebsites() { 19 | const { getToken } = useAuth(); 20 | const [websites, setWebsites] = useState([]); 21 | 22 | async function refreshWebsites() { 23 | const token = await getToken(); 24 | const response = await axios.get(`${API_BACKEND_URL}/api/v1/websites`, { 25 | headers: { 26 | Authorization: token, 27 | }, 28 | }); 29 | 30 | setWebsites(response.data.websites); 31 | } 32 | 33 | useEffect(() => { 34 | refreshWebsites(); 35 | 36 | const interval = setInterval(() => { 37 | refreshWebsites(); 38 | }, 1000 * 60 * 1); 39 | 40 | return () => clearInterval(interval); 41 | }, []); 42 | 43 | return { websites, refreshWebsites }; 44 | 45 | } -------------------------------------------------------------------------------- /apps/frontend/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /apps/frontend/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /apps/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@clerk/nextjs": "^6.12.5", 13 | "@radix-ui/react-slot": "^1.1.2", 14 | "axios": "^1.8.3", 15 | "class-variance-authority": "^0.7.1", 16 | "clsx": "^2.1.1", 17 | "lucide-react": "^0.479.0", 18 | "next": "15.2.2", 19 | "next-themes": "^0.4.6", 20 | "react": "^19.0.0", 21 | "react-dom": "^19.0.0", 22 | "tailwind-merge": "^3.0.2", 23 | "tailwindcss-animate": "^1.0.7" 24 | }, 25 | "devDependencies": { 26 | "@eslint/eslintrc": "^3", 27 | "@tailwindcss/postcss": "^4", 28 | "@types/node": "^20", 29 | "@types/react": "^19", 30 | "@types/react-dom": "^19", 31 | "eslint": "^9", 32 | "eslint-config-next": "15.2.2", 33 | "tailwindcss": "^4", 34 | "typescript": "^5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apps/frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /apps/frontend/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/frontend/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/frontend/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /apps/hub/.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 | -------------------------------------------------------------------------------- /apps/hub/README.md: -------------------------------------------------------------------------------- 1 | # hub 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.2.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /apps/hub/index.ts: -------------------------------------------------------------------------------- 1 | import { randomUUIDv7, type ServerWebSocket } from "bun"; 2 | import type { IncomingMessage, SignupIncomingMessage } from "common/types"; 3 | import { prismaClient } from "db/client"; 4 | import { PublicKey } from "@solana/web3.js"; 5 | import nacl from "tweetnacl"; 6 | import nacl_util from "tweetnacl-util"; 7 | 8 | const availableValidators: { validatorId: string, socket: ServerWebSocket, publicKey: string }[] = []; 9 | 10 | const CALLBACKS : { [callbackId: string]: (data: IncomingMessage) => void } = {} 11 | const COST_PER_VALIDATION = 100; // in lamports 12 | 13 | Bun.serve({ 14 | fetch(req, server) { 15 | if (server.upgrade(req)) { 16 | return; 17 | } 18 | return new Response("Upgrade failed", { status: 500 }); 19 | }, 20 | port: 8081, 21 | websocket: { 22 | async message(ws: ServerWebSocket, message: string) { 23 | const data: IncomingMessage = JSON.parse(message); 24 | 25 | if (data.type === 'signup') { 26 | 27 | const verified = await verifyMessage( 28 | `Signed message for ${data.data.callbackId}, ${data.data.publicKey}`, 29 | data.data.publicKey, 30 | data.data.signedMessage 31 | ); 32 | if (verified) { 33 | await signupHandler(ws, data.data); 34 | } 35 | } else if (data.type === 'validate') { 36 | CALLBACKS[data.data.callbackId](data); 37 | delete CALLBACKS[data.data.callbackId]; 38 | } 39 | }, 40 | async close(ws: ServerWebSocket) { 41 | availableValidators.splice(availableValidators.findIndex(v => v.socket === ws), 1); 42 | } 43 | }, 44 | }); 45 | 46 | async function signupHandler(ws: ServerWebSocket, { ip, publicKey, signedMessage, callbackId }: SignupIncomingMessage) { 47 | const validatorDb = await prismaClient.validator.findFirst({ 48 | where: { 49 | publicKey, 50 | }, 51 | }); 52 | 53 | if (validatorDb) { 54 | ws.send(JSON.stringify({ 55 | type: 'signup', 56 | data: { 57 | validatorId: validatorDb.id, 58 | callbackId, 59 | }, 60 | })); 61 | 62 | availableValidators.push({ 63 | validatorId: validatorDb.id, 64 | socket: ws, 65 | publicKey: validatorDb.publicKey, 66 | }); 67 | return; 68 | } 69 | 70 | //TODO: Given the ip, return the location 71 | const validator = await prismaClient.validator.create({ 72 | data: { 73 | ip, 74 | publicKey, 75 | location: 'unknown', 76 | }, 77 | }); 78 | 79 | ws.send(JSON.stringify({ 80 | type: 'signup', 81 | data: { 82 | validatorId: validator.id, 83 | callbackId, 84 | }, 85 | })); 86 | 87 | availableValidators.push({ 88 | validatorId: validator.id, 89 | socket: ws, 90 | publicKey: validator.publicKey, 91 | }); 92 | } 93 | 94 | async function verifyMessage(message: string, publicKey: string, signature: string) { 95 | const messageBytes = nacl_util.decodeUTF8(message); 96 | const result = nacl.sign.detached.verify( 97 | messageBytes, 98 | new Uint8Array(JSON.parse(signature)), 99 | new PublicKey(publicKey).toBytes(), 100 | ); 101 | 102 | return result; 103 | } 104 | 105 | setInterval(async () => { 106 | const websitesToMonitor = await prismaClient.website.findMany({ 107 | where: { 108 | disabled: false, 109 | }, 110 | }); 111 | 112 | for (const website of websitesToMonitor) { 113 | availableValidators.forEach(validator => { 114 | const callbackId = randomUUIDv7(); 115 | console.log(`Sending validate to ${validator.validatorId} ${website.url}`); 116 | validator.socket.send(JSON.stringify({ 117 | type: 'validate', 118 | data: { 119 | url: website.url, 120 | callbackId 121 | }, 122 | })); 123 | 124 | CALLBACKS[callbackId] = async (data: IncomingMessage) => { 125 | if (data.type === 'validate') { 126 | const { validatorId, status, latency, signedMessage } = data.data; 127 | const verified = await verifyMessage( 128 | `Replying to ${callbackId}`, 129 | validator.publicKey, 130 | signedMessage 131 | ); 132 | if (!verified) { 133 | return; 134 | } 135 | 136 | await prismaClient.$transaction(async (tx) => { 137 | await tx.websiteTick.create({ 138 | data: { 139 | websiteId: website.id, 140 | validatorId, 141 | status, 142 | latency, 143 | createdAt: new Date(), 144 | }, 145 | }); 146 | 147 | await tx.validator.update({ 148 | where: { id: validatorId }, 149 | data: { 150 | pendingPayouts: { increment: COST_PER_VALIDATION }, 151 | }, 152 | }); 153 | }); 154 | } 155 | }; 156 | }); 157 | } 158 | }, 60 * 1000); -------------------------------------------------------------------------------- /apps/hub/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hub", 3 | "module": "index.ts", 4 | "type": "module", 5 | "devDependencies": { 6 | "@types/bun": "latest", 7 | "common": "*", 8 | "db": "*" 9 | }, 10 | "peerDependencies": { 11 | "typescript": "^5.0.0" 12 | }, 13 | "dependencies": { 14 | "@solana/kit": "^2.1.0", 15 | "@solana/web3.js": "^1.98.0", 16 | "tweetnacl": "^1.0.3" 17 | } 18 | } -------------------------------------------------------------------------------- /apps/hub/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 | -------------------------------------------------------------------------------- /apps/validator/.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 | -------------------------------------------------------------------------------- /apps/validator/README.md: -------------------------------------------------------------------------------- 1 | # validator 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.2.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /apps/validator/index.ts: -------------------------------------------------------------------------------- 1 | import { randomUUIDv7 } from "bun"; 2 | import type { OutgoingMessage, SignupOutgoingMessage, ValidateOutgoingMessage } from "common/types"; 3 | import { Keypair } from "@solana/web3.js"; 4 | import nacl from "tweetnacl"; 5 | import nacl_util from "tweetnacl-util"; 6 | 7 | const CALLBACKS: {[callbackId: string]: (data: SignupOutgoingMessage) => void} = {} 8 | 9 | let validatorId: string | null = null; 10 | 11 | async function main() { 12 | const keypair = Keypair.fromSecretKey( 13 | Uint8Array.from(JSON.parse(process.env.PRIVATE_KEY!)) 14 | ); 15 | const ws = new WebSocket("ws://localhost:8081"); 16 | 17 | ws.onmessage = async (event) => { 18 | const data: OutgoingMessage = JSON.parse(event.data); 19 | if (data.type === 'signup') { 20 | CALLBACKS[data.data.callbackId]?.(data.data) 21 | delete CALLBACKS[data.data.callbackId]; 22 | } else if (data.type === 'validate') { 23 | await validateHandler(ws, data.data, keypair); 24 | } 25 | } 26 | 27 | ws.onopen = async () => { 28 | const callbackId = randomUUIDv7(); 29 | CALLBACKS[callbackId] = (data: SignupOutgoingMessage) => { 30 | validatorId = data.validatorId; 31 | } 32 | const signedMessage = await signMessage(`Signed message for ${callbackId}, ${keypair.publicKey}`, keypair); 33 | 34 | ws.send(JSON.stringify({ 35 | type: 'signup', 36 | data: { 37 | callbackId, 38 | ip: '127.0.0.1', 39 | publicKey: keypair.publicKey, 40 | signedMessage, 41 | }, 42 | })); 43 | } 44 | } 45 | 46 | async function validateHandler(ws: WebSocket, { url, callbackId, websiteId }: ValidateOutgoingMessage, keypair: Keypair) { 47 | console.log(`Validating ${url}`); 48 | const startTime = Date.now(); 49 | const signature = await signMessage(`Replying to ${callbackId}`, keypair); 50 | 51 | try { 52 | const response = await fetch(url); 53 | const endTime = Date.now(); 54 | const latency = endTime - startTime; 55 | const status = response.status; 56 | 57 | console.log(url); 58 | console.log(status); 59 | ws.send(JSON.stringify({ 60 | type: 'validate', 61 | data: { 62 | callbackId, 63 | status: status === 200 ? 'Good' : 'Bad', 64 | latency, 65 | websiteId, 66 | validatorId, 67 | signedMessage: signature, 68 | }, 69 | })); 70 | } catch (error) { 71 | ws.send(JSON.stringify({ 72 | type: 'validate', 73 | data: { 74 | callbackId, 75 | status:'Bad', 76 | latency: 1000, 77 | websiteId, 78 | validatorId, 79 | signedMessage: signature, 80 | }, 81 | })); 82 | console.error(error); 83 | } 84 | } 85 | 86 | async function signMessage(message: string, keypair: Keypair) { 87 | const messageBytes = nacl_util.decodeUTF8(message); 88 | const signature = nacl.sign.detached(messageBytes, keypair.secretKey); 89 | 90 | return JSON.stringify(Array.from(signature)); 91 | } 92 | 93 | main(); 94 | 95 | setInterval(async () => { 96 | 97 | }, 10000); -------------------------------------------------------------------------------- /apps/validator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "validator", 3 | "module": "index.ts", 4 | "type": "module", 5 | "devDependencies": { 6 | "@types/bun": "latest", 7 | "common": "*" 8 | }, 9 | "peerDependencies": { 10 | "typescript": "^5.0.0" 11 | }, 12 | "dependencies": { 13 | "@solana/kit": "^2.1.0", 14 | "@solana/web3.js": "^1.98.0", 15 | "tweetnacl": "^1.0.3", 16 | "tweetnacl-util": "^0.15.1" 17 | } 18 | } -------------------------------------------------------------------------------- /apps/validator/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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dpin-uptime", 3 | "private": true, 4 | "scripts": { 5 | "build": "turbo run build", 6 | "dev": "turbo run dev", 7 | "lint": "turbo run lint", 8 | "format": "prettier --write \"**/*.{ts,tsx,md}\"", 9 | "check-types": "turbo run check-types" 10 | }, 11 | "devDependencies": { 12 | "prettier": "^3.5.3", 13 | "turbo": "^2.4.4", 14 | "typescript": "5.8.2" 15 | }, 16 | "engines": { 17 | "node": ">=18" 18 | }, 19 | "packageManager": "bun@1.2.2", 20 | "workspaces": [ 21 | "apps/*", 22 | "packages/*" 23 | ], 24 | "dependencies": { 25 | "prisma": "^6.5.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/common/.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 | -------------------------------------------------------------------------------- /packages/common/README.md: -------------------------------------------------------------------------------- 1 | # common 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.2.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /packages/common/index.ts: -------------------------------------------------------------------------------- 1 | export interface SignupIncomingMessage { 2 | ip: string; 3 | publicKey: string; 4 | signedMessage: string; 5 | callbackId: string; 6 | } 7 | 8 | export interface ValidateIncomingMessage { 9 | callbackId: string; 10 | signedMessage: string; 11 | status: 'Good' | 'Bad'; 12 | latency: number; 13 | websiteId: string; 14 | validatorId: string; 15 | } 16 | 17 | export interface SignupOutgoingMessage { 18 | validatorId: string; 19 | callbackId: string; 20 | } 21 | 22 | export interface ValidateOutgoingMessage { 23 | url: string, 24 | callbackId: string, 25 | websiteId: string; 26 | } 27 | 28 | export type IncomingMessage = { 29 | type: 'signup' 30 | data: SignupIncomingMessage 31 | } | { 32 | type: 'validate' 33 | data: ValidateIncomingMessage 34 | } 35 | 36 | export type OutgoingMessage = { 37 | type: 'signup' 38 | data: SignupOutgoingMessage 39 | } | { 40 | type: 'validate' 41 | data: ValidateOutgoingMessage 42 | } 43 | -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "common", 3 | "module": "index.ts", 4 | "type": "module", 5 | "exports": { 6 | "./types": "./index.ts" 7 | }, 8 | "devDependencies": { 9 | "@types/bun": "latest" 10 | }, 11 | "peerDependencies": { 12 | "typescript": "^5.0.0" 13 | } 14 | } -------------------------------------------------------------------------------- /packages/common/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 | -------------------------------------------------------------------------------- /packages/db/.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 | -------------------------------------------------------------------------------- /packages/db/README.md: -------------------------------------------------------------------------------- 1 | # db 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.2.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /packages/db/index.ts: -------------------------------------------------------------------------------- 1 | console.log("Hello via Bun!"); -------------------------------------------------------------------------------- /packages/db/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "db", 3 | "module": "index.ts", 4 | "type": "module", 5 | "exports": { 6 | "./client": "./src/index.ts" 7 | }, 8 | "devDependencies": { 9 | "@types/bun": "latest" 10 | }, 11 | "peerDependencies": { 12 | "typescript": "^5.0.0" 13 | }, 14 | "dependencies": { 15 | "prisma": "^6.5.0" 16 | }, 17 | "prisma": { 18 | "seed": "./prisma/seed.ts" 19 | } 20 | } -------------------------------------------------------------------------------- /packages/db/prisma/migrations/20250314011543_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "WebsiteStatus" AS ENUM ('Good', 'Bad'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "User" ( 6 | "id" TEXT NOT NULL, 7 | "email" TEXT NOT NULL, 8 | 9 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 10 | ); 11 | 12 | -- CreateTable 13 | CREATE TABLE "Website" ( 14 | "id" TEXT NOT NULL, 15 | "url" TEXT NOT NULL, 16 | "userId" TEXT NOT NULL, 17 | 18 | CONSTRAINT "Website_pkey" PRIMARY KEY ("id") 19 | ); 20 | 21 | -- CreateTable 22 | CREATE TABLE "Validator" ( 23 | "id" TEXT NOT NULL, 24 | "publicKey" TEXT NOT NULL, 25 | "location" TEXT NOT NULL, 26 | "ip" TEXT NOT NULL, 27 | 28 | CONSTRAINT "Validator_pkey" PRIMARY KEY ("id") 29 | ); 30 | 31 | -- CreateTable 32 | CREATE TABLE "WebsiteTick" ( 33 | "id" TEXT NOT NULL, 34 | "websiteId" TEXT NOT NULL, 35 | "validatorId" TEXT NOT NULL, 36 | "createdAt" TIMESTAMP(3) NOT NULL, 37 | "status" "WebsiteStatus" NOT NULL, 38 | "latency" DOUBLE PRECISION NOT NULL, 39 | 40 | CONSTRAINT "WebsiteTick_pkey" PRIMARY KEY ("id") 41 | ); 42 | 43 | -- AddForeignKey 44 | ALTER TABLE "WebsiteTick" ADD CONSTRAINT "WebsiteTick_websiteId_fkey" FOREIGN KEY ("websiteId") REFERENCES "Website"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 45 | 46 | -- AddForeignKey 47 | ALTER TABLE "WebsiteTick" ADD CONSTRAINT "WebsiteTick_validatorId_fkey" FOREIGN KEY ("validatorId") REFERENCES "Validator"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 48 | -------------------------------------------------------------------------------- /packages/db/prisma/migrations/20250314012859_added_disabled/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Website" ADD COLUMN "disabled" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /packages/db/prisma/migrations/20250315042613_added_payout/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "pendingPayouts" INTEGER NOT NULL DEFAULT 0; 3 | -------------------------------------------------------------------------------- /packages/db/prisma/migrations/20250315042714_fix_payout/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `pendingPayouts` on the `User` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "User" DROP COLUMN "pendingPayouts"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "Validator" ADD COLUMN "pendingPayouts" INTEGER NOT NULL DEFAULT 0; 12 | -------------------------------------------------------------------------------- /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 (e.g., Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /packages/db/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | model User { 17 | id String @id @default(uuid()) 18 | email String 19 | } 20 | 21 | model Website { 22 | id String @id @default(uuid()) 23 | url String 24 | userId String 25 | ticks WebsiteTick[] 26 | disabled Boolean @default(false) 27 | } 28 | 29 | model Validator { 30 | id String @id @default(uuid()) 31 | publicKey String 32 | location String 33 | ip String 34 | pendingPayouts Int @default(0) 35 | ticks WebsiteTick[] 36 | } 37 | 38 | model WebsiteTick { 39 | id String @id @default(uuid()) 40 | websiteId String 41 | validatorId String 42 | createdAt DateTime 43 | status WebsiteStatus 44 | latency Float 45 | website Website @relation(fields: [websiteId], references: [id]) 46 | validator Validator @relation(fields: [validatorId], references: [id]) 47 | } 48 | 49 | enum WebsiteStatus { 50 | Good 51 | Bad 52 | } -------------------------------------------------------------------------------- /packages/db/prisma/seed.ts: -------------------------------------------------------------------------------- 1 | 2 | import { prismaClient } from "../src"; 3 | 4 | const USER_ID = "4"; 5 | 6 | async function seed() { 7 | await prismaClient.user.create({ 8 | data: { 9 | id: USER_ID, 10 | email: "test@test.com", 11 | } 12 | }) 13 | 14 | const website = await prismaClient.website.create({ 15 | data: { 16 | url: "https://test.com", 17 | userId: USER_ID 18 | } 19 | }) 20 | 21 | const validator = await prismaClient.validator.create({ 22 | data: { 23 | publicKey: "0x12341223123", 24 | location: "Delhi", 25 | ip: "127.0.0.1", 26 | } 27 | }) 28 | 29 | await prismaClient.websiteTick.create({ 30 | data: { 31 | websiteId: website.id, 32 | status: "Good", 33 | createdAt: new Date(), 34 | latency: 100, 35 | validatorId: validator.id 36 | } 37 | }) 38 | 39 | await prismaClient.websiteTick.create({ 40 | data: { 41 | websiteId: website.id, 42 | status: "Good", 43 | createdAt: new Date(Date.now() - 1000 * 60 *10), 44 | latency: 100, 45 | validatorId: validator.id 46 | } 47 | }) 48 | 49 | await prismaClient.websiteTick.create({ 50 | data: { 51 | websiteId: website.id, 52 | status: "Bad", 53 | createdAt: new Date(Date.now() - 1000 * 60 * 20), 54 | latency: 100, 55 | validatorId: validator.id 56 | } 57 | }) 58 | } 59 | 60 | seed(); -------------------------------------------------------------------------------- /packages/db/src/index.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client" 2 | 3 | export const prismaClient = new PrismaClient() 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/db/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 | -------------------------------------------------------------------------------- /packages/eslint-config/README.md: -------------------------------------------------------------------------------- 1 | # `@turbo/eslint-config` 2 | 3 | Collection of internal eslint configurations. 4 | -------------------------------------------------------------------------------- /packages/eslint-config/base.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import eslintConfigPrettier from "eslint-config-prettier"; 3 | import turboPlugin from "eslint-plugin-turbo"; 4 | import tseslint from "typescript-eslint"; 5 | import onlyWarn from "eslint-plugin-only-warn"; 6 | 7 | /** 8 | * A shared ESLint configuration for the repository. 9 | * 10 | * @type {import("eslint").Linter.Config[]} 11 | * */ 12 | export const config = [ 13 | js.configs.recommended, 14 | eslintConfigPrettier, 15 | ...tseslint.configs.recommended, 16 | { 17 | plugins: { 18 | turbo: turboPlugin, 19 | }, 20 | rules: { 21 | "turbo/no-undeclared-env-vars": "warn", 22 | }, 23 | }, 24 | { 25 | plugins: { 26 | onlyWarn, 27 | }, 28 | }, 29 | { 30 | ignores: ["dist/**"], 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /packages/eslint-config/next.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import eslintConfigPrettier from "eslint-config-prettier"; 3 | import tseslint from "typescript-eslint"; 4 | import pluginReactHooks from "eslint-plugin-react-hooks"; 5 | import pluginReact from "eslint-plugin-react"; 6 | import globals from "globals"; 7 | import pluginNext from "@next/eslint-plugin-next"; 8 | import { config as baseConfig } from "./base.js"; 9 | 10 | /** 11 | * A custom ESLint configuration for libraries that use Next.js. 12 | * 13 | * @type {import("eslint").Linter.Config[]} 14 | * */ 15 | export const nextJsConfig = [ 16 | ...baseConfig, 17 | js.configs.recommended, 18 | eslintConfigPrettier, 19 | ...tseslint.configs.recommended, 20 | { 21 | ...pluginReact.configs.flat.recommended, 22 | languageOptions: { 23 | ...pluginReact.configs.flat.recommended.languageOptions, 24 | globals: { 25 | ...globals.serviceworker, 26 | }, 27 | }, 28 | }, 29 | { 30 | plugins: { 31 | "@next/next": pluginNext, 32 | }, 33 | rules: { 34 | ...pluginNext.configs.recommended.rules, 35 | ...pluginNext.configs["core-web-vitals"].rules, 36 | }, 37 | }, 38 | { 39 | plugins: { 40 | "react-hooks": pluginReactHooks, 41 | }, 42 | settings: { react: { version: "detect" } }, 43 | rules: { 44 | ...pluginReactHooks.configs.recommended.rules, 45 | // React scope no longer necessary with new JSX transform. 46 | "react/react-in-jsx-scope": "off", 47 | }, 48 | }, 49 | ]; 50 | -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/eslint-config", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "private": true, 6 | "exports": { 7 | "./base": "./base.js", 8 | "./next-js": "./next.js", 9 | "./react-internal": "./react-internal.js" 10 | }, 11 | "devDependencies": { 12 | "@eslint/js": "^9.22.0", 13 | "@next/eslint-plugin-next": "^15.2.1", 14 | "eslint": "^9.22.0", 15 | "eslint-config-prettier": "^10.1.1", 16 | "eslint-plugin-only-warn": "^1.1.0", 17 | "eslint-plugin-react": "^7.37.4", 18 | "eslint-plugin-react-hooks": "^5.2.0", 19 | "eslint-plugin-turbo": "^2.4.4", 20 | "globals": "^16.0.0", 21 | "typescript": "^5.8.2", 22 | "typescript-eslint": "^8.26.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/eslint-config/react-internal.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import eslintConfigPrettier from "eslint-config-prettier"; 3 | import tseslint from "typescript-eslint"; 4 | import pluginReactHooks from "eslint-plugin-react-hooks"; 5 | import pluginReact from "eslint-plugin-react"; 6 | import globals from "globals"; 7 | import { config as baseConfig } from "./base.js"; 8 | 9 | /** 10 | * A custom ESLint configuration for libraries that use React. 11 | * 12 | * @type {import("eslint").Linter.Config[]} */ 13 | export const config = [ 14 | ...baseConfig, 15 | js.configs.recommended, 16 | eslintConfigPrettier, 17 | ...tseslint.configs.recommended, 18 | pluginReact.configs.flat.recommended, 19 | { 20 | languageOptions: { 21 | ...pluginReact.configs.flat.recommended.languageOptions, 22 | globals: { 23 | ...globals.serviceworker, 24 | ...globals.browser, 25 | }, 26 | }, 27 | }, 28 | { 29 | plugins: { 30 | "react-hooks": pluginReactHooks, 31 | }, 32 | settings: { react: { version: "detect" } }, 33 | rules: { 34 | ...pluginReactHooks.configs.recommended.rules, 35 | // React scope no longer necessary with new JSX transform. 36 | "react/react-in-jsx-scope": "off", 37 | }, 38 | }, 39 | ]; 40 | -------------------------------------------------------------------------------- /packages/typescript-config/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationMap": true, 6 | "esModuleInterop": true, 7 | "incremental": false, 8 | "isolatedModules": true, 9 | "lib": ["es2022", "DOM", "DOM.Iterable"], 10 | "module": "NodeNext", 11 | "moduleDetection": "force", 12 | "moduleResolution": "NodeNext", 13 | "noUncheckedIndexedAccess": true, 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true, 16 | "strict": true, 17 | "target": "ES2022" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/typescript-config/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "plugins": [{ "name": "next" }], 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler", 8 | "allowJs": true, 9 | "jsx": "preserve", 10 | "noEmit": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /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 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "jsx": "react-jsx" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { config } from "@repo/eslint-config/react-internal"; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/ui", 3 | "version": "0.0.0", 4 | "private": true, 5 | "exports": { 6 | "./*": "./src/*.tsx" 7 | }, 8 | "scripts": { 9 | "lint": "eslint . --max-warnings 0", 10 | "generate:component": "turbo gen react-component", 11 | "check-types": "tsc --noEmit" 12 | }, 13 | "devDependencies": { 14 | "@repo/eslint-config": "*", 15 | "@repo/typescript-config": "*", 16 | "@turbo/gen": "^2.4.4", 17 | "@types/node": "^22.13.9", 18 | "@types/react": "19.0.10", 19 | "@types/react-dom": "19.0.4", 20 | "eslint": "^9.22.0", 21 | "typescript": "5.8.2" 22 | }, 23 | "dependencies": { 24 | "react": "^19.0.0", 25 | "react-dom": "^19.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/ui/src/button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ReactNode } from "react"; 4 | 5 | interface ButtonProps { 6 | children: ReactNode; 7 | className?: string; 8 | appName: string; 9 | } 10 | 11 | export const Button = ({ children, className, appName }: ButtonProps) => { 12 | return ( 13 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/ui/src/card.tsx: -------------------------------------------------------------------------------- 1 | import { type JSX } from "react"; 2 | 3 | export function Card({ 4 | className, 5 | title, 6 | children, 7 | href, 8 | }: { 9 | className?: string; 10 | title: string; 11 | children: React.ReactNode; 12 | href: string; 13 | }): JSX.Element { 14 | return ( 15 | 21 |

22 | {title} -> 23 |

24 |

{children}

25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /packages/ui/src/code.tsx: -------------------------------------------------------------------------------- 1 | import { type JSX } from "react"; 2 | 3 | export function Code({ 4 | children, 5 | className, 6 | }: { 7 | children: React.ReactNode; 8 | className?: string; 9 | }): JSX.Element { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "ui": "tui", 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "inputs": ["$TURBO_DEFAULT$", ".env*"], 8 | "outputs": [".next/**", "!.next/cache/**"] 9 | }, 10 | "lint": { 11 | "dependsOn": ["^lint"] 12 | }, 13 | "check-types": { 14 | "dependsOn": ["^check-types"] 15 | }, 16 | "dev": { 17 | "cache": false, 18 | "persistent": true 19 | } 20 | } 21 | } 22 | --------------------------------------------------------------------------------