├── .env.example ├── public ├── favicon.ico ├── og-image.png ├── newIcon-white.svg ├── newIcon.svg └── vercel.svg ├── postcss.config.js ├── next.config.js ├── tailwind.config.ts ├── utils ├── prisma.ts └── utils.ts ├── styles └── globals.css ├── app ├── page.tsx ├── layout.tsx └── ClientPage.tsx ├── .gitignore ├── components ├── Icons │ ├── WebsiteIcon.tsx │ ├── TwitterIcon.tsx │ └── CheckIcon.tsx ├── SearchBar.tsx ├── Header.tsx ├── Stats.tsx ├── Footer.tsx └── InvestorTable.tsx ├── README.md ├── tsconfig.json ├── package.json └── prisma └── schema.prisma /.env.example: -------------------------------------------------------------------------------- 1 | POSTGRES_PRISMA_URL= 2 | POSTGRES_URL_NON_POOLING= 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exception/aiangels/main/public/favicon.ico -------------------------------------------------------------------------------- /public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exception/aiangels/main/public/og-image.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | images: { 6 | domains: ["pbs.twimg.com", "media-exp1.licdn.com"], 7 | }, 8 | }; 9 | 10 | module.exports = nextConfig; 11 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | plugins: [], 10 | }; 11 | export default config; 12 | -------------------------------------------------------------------------------- /utils/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | declare global { 4 | var prisma: PrismaClient | undefined; 5 | } 6 | 7 | const client = globalThis.prisma || new PrismaClient(); 8 | if (process.env.NODE_ENV !== 'production') globalThis.prisma = client; 9 | 10 | export default client; 11 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* Hide scrollbar for Chrome, Safari and Opera */ 6 | body::-webkit-scrollbar { 7 | display: none; 8 | } 9 | 10 | /* Hide scrollbar for IE, Edge and Firefox */ 11 | body { 12 | -ms-overflow-style: none; /* IE and Edge */ 13 | scrollbar-width: none; /* Firefox */ 14 | } 15 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Dashboard from './ClientPage'; 2 | import prisma from '../utils/prisma'; 3 | import { cache } from 'react'; 4 | 5 | export const revalidate = 86400; // revalidate the data at most every 24 hours 6 | 7 | const getAllAngels = cache(async () => { 8 | const data = await prisma.investor.findMany({}); 9 | return data; 10 | }); 11 | 12 | export default async function HomePage() { 13 | const data = await getAllAngels(); 14 | 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /public/newIcon-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.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 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /components/Icons/WebsiteIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function WebsiteIcon(props: any) { 2 | return ( 3 | 12 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [aiangels.fund](https://www.aiangels.fund) 2 | 3 | A list of active angel investors that invest in AI startups. 4 | 5 | [![AI Angels screenshot](./public/og-image.png)](https://aiangels.fund/) 6 | 7 | ## Powered by 8 | 9 | This example is powered by the following services: 10 | 11 | - Next.js App Router (Framework) 12 | - Vercel Postgres & Prisma (Database) 13 | - Vercel (Analytics and hosting) 14 | - Tailwind (CSS Framework) 15 | 16 | ## Future Tasks 17 | 18 | - [ ] Dark mode 19 | - [ ] Profile pages with a full angel investor profile. Should include all the info in the table + maybe sections like previous investments 20 | -------------------------------------------------------------------------------- /components/Icons/TwitterIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function TwitterIcon(props: any) { 2 | return ( 3 | 11 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "incremental": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve", 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ], 26 | "strictNullChecks": true 27 | }, 28 | "include": [ 29 | "next-env.d.ts", 30 | "**/*.ts", 31 | "**/*.tsx", 32 | ".next/types/**/*.ts" 33 | ], 34 | "exclude": [ 35 | "node_modules" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-angels", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "vercel-build": "prisma generate && next build", 11 | "prisma:generate": "prisma generate" 12 | }, 13 | "dependencies": { 14 | "@prisma/client": "^5.3.1", 15 | "@vercel/analytics": "^1.0.2", 16 | "fuse.js": "^6.6.2", 17 | "next": "^13.4.19", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-highlight-words": "^0.18.0" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "18.7.18", 24 | "@types/react": "^18.2.22", 25 | "autoprefixer": "^10.4.11", 26 | "postcss": "^8.4.16", 27 | "prisma": "^5.3.1", 28 | "tailwindcss": "^3.3.3", 29 | "typescript": "^5.2.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /public/newIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("POSTGRES_PRISMA_URL") // uses connection pooling 8 | directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection 9 | } 10 | 11 | model Investor { 12 | id Int @id @default(autoincrement()) 13 | name String 14 | email String? 15 | company String? 16 | title String? 17 | checkSize String? 18 | details String? 19 | twitterPicture String? 20 | site String? 21 | twitterVerified Boolean? @default(false) 22 | hidden Boolean? @default(false) 23 | rank Int? @default(0) 24 | createdAt DateTime @default(now()) 25 | updatedAt DateTime @updatedAt 26 | checksize CheckSize? @relation(fields: [checksize_id], references: [id]) 27 | checksize_id Int? 28 | } 29 | 30 | model CheckSize { 31 | id Int @unique 32 | name String @unique 33 | angels Investor[] 34 | } 35 | -------------------------------------------------------------------------------- /components/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | export default function SearchBar({ search, setSearch }) { 2 | return ( 3 |
4 |
18 | ); 19 | } 20 | 21 | function SearchIcon(props: any) { 22 | return ( 23 | 35 | 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | 4 | export default function Header() { 5 | return ( 6 | <> 7 | 25 |
26 |
27 |

28 | Find the next angel investor for your AI startup 29 |

30 |
31 |
32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import { Analytics } from '@vercel/analytics/react'; 3 | import '../styles/globals.css'; 4 | import Header from '../components/Header'; 5 | 6 | let title = 'AI Angel Investors'; 7 | let description = 'Find your next AI angel'; 8 | let url = 'https://www.aiangels.fund'; 9 | let ogimage = 'https://www.aiangels.fund/og-image.png'; 10 | let sitename = 'aiangels.fund'; 11 | 12 | export const metadata: Metadata = { 13 | metadataBase: new URL(url), 14 | title, 15 | description, 16 | icons: { 17 | icon: '/favicon.ico', 18 | }, 19 | openGraph: { 20 | images: [ogimage], 21 | title, 22 | description, 23 | url: url, 24 | siteName: sitename, 25 | locale: 'en_US', 26 | type: 'website', 27 | }, 28 | twitter: { 29 | card: 'summary_large_image', 30 | images: [ogimage], 31 | title, 32 | description, 33 | }, 34 | }; 35 | 36 | export default function RootLayout({ 37 | children, 38 | }: { 39 | children: React.ReactNode; 40 | }) { 41 | return ( 42 | 43 | 44 |
45 |
46 |
47 | {children} 48 |
49 |
50 | 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /components/Icons/CheckIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function CheckIcon(props: any) { 2 | return ( 3 | 10 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /utils/utils.ts: -------------------------------------------------------------------------------- 1 | export function compare(a: any, b: any) { 2 | // if (a.twitterVerified === true && b.twitterVerified !== true) { 3 | // return -1; 4 | // } 5 | // if (a.twitterVerified !== true && b.twitterVerified === true) { 6 | // return 1; 7 | // } 8 | if (a.checksize_id > b.checksize_id) { 9 | return -1; 10 | } 11 | if (a.checksize_id < b.checksize_id) { 12 | return 1; 13 | } 14 | return 0; 15 | } 16 | 17 | export function kFormatter(num: any) { 18 | return Math.abs(num) > 4000 19 | ? Math.sign(num) * Number((Math.abs(num) / 1000).toFixed(0)) + 'k' 20 | : Math.sign(num) * Math.abs(num); 21 | } 22 | 23 | export const checkSizeMap = { 24 | 0: 'Unknown', 25 | 1: '$2-5k', 26 | 2: '$5-15k', 27 | 3: '$15-25k', 28 | 4: '$25-50k', 29 | 5: '$50-100k', 30 | 6: '$100k+', 31 | 7: 'All', 32 | }; 33 | 34 | let checksizes = { 35 | 0: 0, 36 | 1: 3500, 37 | 2: 10000, 38 | 3: 20000, 39 | 4: 37500, 40 | 5: 75000, 41 | 6: 100000, 42 | }; 43 | export function getCheckSizeForId(id: keyof typeof checksizes) { 44 | return checksizes[id]; 45 | } 46 | 47 | export const checkSizes = [ 48 | { id: '7', label: 'All' }, 49 | { id: '2', label: '$5-15k' }, 50 | { id: '3', label: '$15-25k' }, 51 | { id: '4', label: '$25-50k' }, 52 | { id: '6', label: '$100k' }, 53 | ]; 54 | 55 | export function classNames(...classes: string[]) { 56 | return classes.filter(Boolean).join(' '); 57 | } 58 | 59 | export const searchOptions = { 60 | threshold: 0.3, 61 | location: 0, 62 | distance: 100, 63 | minMatchCharLength: 2, 64 | keys: ['name', 'email', 'company', 'title', 'details'], 65 | }; 66 | -------------------------------------------------------------------------------- /components/Stats.tsx: -------------------------------------------------------------------------------- 1 | import { kFormatter } from '../utils/utils'; 2 | 3 | export default function Stats({ angelsLength, averageCheck, companiesLength }) { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 | Angel Investors 12 |
13 |
14 | {angelsLength} 15 |
16 |
17 |
18 |
19 | Average Check Size 20 |
21 |
22 | {kFormatter(averageCheck) ? '$' + kFormatter(averageCheck) : '$0'} 23 |
24 |
25 |
26 |
27 | Confirmed Investments 28 |
29 |
30 | {(angelsLength * 2.5).toFixed(0)}+ 31 |
32 |
33 |
34 |
35 | Companies 36 |
37 |
38 | {companiesLength} 39 |
40 |
41 |
42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import CheckIcon from './Icons/CheckIcon'; 3 | 4 | export default function Footer() { 5 | return ( 6 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /app/ClientPage.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Fuse from 'fuse.js'; 4 | import Link from 'next/link'; 5 | import { useSearchParams } from 'next/navigation'; 6 | import { useMemo, useState } from 'react'; 7 | import CheckIcon from '../components/Icons/CheckIcon'; 8 | import InvestorTable from '../components/InvestorTable'; 9 | import SearchBar from '../components/SearchBar'; 10 | import Stats from '../components/Stats'; 11 | import { 12 | checkSizes, 13 | classNames, 14 | compare, 15 | getCheckSizeForId, 16 | searchOptions, 17 | } from '../utils/utils'; 18 | import Footer from '../components/Footer'; 19 | 20 | export default function Dashboard({ data }: any) { 21 | const allAngels = data; 22 | const [search, setSearch] = useState(''); 23 | 24 | const searchParams = useSearchParams(); 25 | const category = searchParams!.get('category'); 26 | 27 | // Define filtered & sorted angels array 28 | const ALL_ANGELS = allAngels 29 | .filter((angel: any) => !angel.hidden) 30 | .sort(compare) 31 | .filter((person: any) => { 32 | return !category ? true : person.checksize_id.toString() === category; 33 | }); 34 | 35 | // Fuzzy search with highlighting 36 | const fuse = new Fuse(ALL_ANGELS, searchOptions); 37 | const angels = useMemo(() => { 38 | if (search.length > 0) { 39 | return fuse.search(search).map((match) => match.item); 40 | } 41 | return ALL_ANGELS; 42 | }, [search, ALL_ANGELS]); 43 | 44 | // Get stats 45 | const companies = [...new Set(angels.map((angel: any) => angel.company))]; 46 | const allChecksizes = angels 47 | .filter((angel: any) => angel.checksize_id) 48 | .map((angel: any) => getCheckSizeForId(angel.checksize_id)); 49 | const averageCheck = 50 | allChecksizes.reduce((a: number, b: number) => a + b, 0) / 51 | allChecksizes.length; 52 | 53 | return ( 54 | <> 55 | 60 |
61 | 62 | {checkSizes.map((checkSize) => ( 63 | 73 | {checkSize.label} 74 | 75 | ))} 76 | 77 | 78 |
79 |
80 |
81 |
82 |
83 | 84 |
85 | {/*
*/} 86 |
87 | {/*
*/} 88 |
89 |
90 |
91 | 92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /components/InvestorTable.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Highlighter from 'react-highlight-words'; 3 | import { checkSizeMap, classNames } from '../utils/utils'; 4 | import CheckIcon from './Icons/CheckIcon'; 5 | import TwitterIcon from './Icons/TwitterIcon'; 6 | import WebsiteIcon from './Icons/WebsiteIcon'; 7 | 8 | export default function InvestorTable({ angels, search }) { 9 | return ( 10 |
11 | 12 | 13 | 14 | 20 | 26 | 32 | 38 | 44 | 45 | 46 | 47 | {angels.map((person: any) => ( 48 | 52 | 99 | 106 | 113 | 133 | 140 | 141 | ))} 142 | 143 |
18 | Name 19 | 24 | Company 25 | 30 | Title 31 | 36 | Check Size 37 | 42 | Details 43 |
53 |
54 |
55 | twitter avatar 62 |
63 |
64 |
65 | 70 | {person.twitterVerified && ( 71 | 72 | )} 73 |
74 |
75 | 81 | Twitter 82 | 83 | 84 | {person.site && ( 85 | 91 | Website 92 | 93 | 94 | )} 95 |
96 |
97 |
98 |
100 | 105 | 107 | 112 | 114 | 130 | {checkSizeMap[person.checksize_id]} 131 | 132 | 134 | 139 |
144 | {angels.length === 0 && ( 145 |
No results found
146 | )} 147 |
148 | ); 149 | } 150 | --------------------------------------------------------------------------------