├── .eslintrc.json
├── .github
├── funding.yml
└── workflows
│ └── test.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.cjs
├── components
├── ActiveLink
│ └── ActiveLink.tsx
├── Button
│ ├── Button.tsx
│ └── styles.module.css
├── Footer
│ ├── Footer.tsx
│ └── styles.module.css
├── Header
│ ├── Header.tsx
│ ├── Logo.tsx
│ └── styles.module.css
├── HeroButton
│ ├── HeroButton.tsx
│ └── styles.module.css
├── Layout
│ ├── Layout.tsx
│ ├── providers.tsx
│ └── styles.module.css
├── Markdown
│ ├── Markdown.tsx
│ └── styles.module.css
├── MetaballVisualization
│ ├── Body.ts
│ ├── MetaballVisualization.tsx
│ ├── MetaballViz.ts
│ └── styles.module.css
├── PageHead
│ └── PageHead.tsx
└── WebGLSupportChecker
│ └── WebGLSupportChecker.tsx
├── icons
├── Discord.tsx
├── GitHub.tsx
├── Twitter.tsx
└── index.ts
├── lib
├── bootstrap.ts
├── config.ts
└── markdown-to-html.ts
├── license
├── next.config.mjs
├── package.json
├── pages
├── _app.tsx
├── _document.tsx
├── about
│ ├── index.tsx
│ └── styles.module.css
├── index.module.css
└── index.tsx
├── pnpm-lock.yaml
├── postcss.config.cjs
├── public
├── favicon.ico
├── icon.png
├── icon.svg
├── logo-dark.png
├── logo-light.png
├── robots.txt
└── social.jpg
├── readme.md
├── store
└── metaballs.ts
├── styles
└── globals.css
├── tailwind.config.cjs
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/funding.yml:
--------------------------------------------------------------------------------
1 | github: [transitive-bullshit]
2 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | name: Test Node.js ${{ matrix.node-version }}
8 | runs-on: ubuntu-latest
9 | strategy:
10 | fail-fast: false
11 | matrix:
12 | node-version:
13 | - 19
14 | - 18
15 | - 16
16 |
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v3
20 |
21 | - name: Install Node.js
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: ${{ matrix.node-version }}
25 |
26 | - name: Install pnpm
27 | uses: pnpm/action-setup@v2
28 | id: pnpm-install
29 | with:
30 | version: 7
31 | run_install: false
32 |
33 | - name: Get pnpm store directory
34 | id: pnpm-cache
35 | shell: bash
36 | run: |
37 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
38 |
39 | - uses: actions/cache@v3
40 | name: Setup pnpm cache
41 | with:
42 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
43 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
44 | restore-keys: |
45 | ${{ runner.os }}-pnpm-store-
46 |
47 | - name: Install dependencies
48 | run: pnpm install --frozen-lockfile
49 |
50 | - name: Run test
51 | run: pnpm run test
52 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | package-lock.json
2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
3 |
4 | # dependencies
5 | /node_modules
6 | /.pnp
7 | .pnp.js
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 | .pnpm-debug.log*
28 |
29 | # local env files
30 | .env*.local
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /.next/
2 |
--------------------------------------------------------------------------------
/.prettierrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [require('@trivago/prettier-plugin-sort-imports')],
3 | singleQuote: true,
4 | jsxSingleQuote: true,
5 | semi: false,
6 | useTabs: false,
7 | tabWidth: 2,
8 | bracketSpacing: true,
9 | bracketSameLine: false,
10 | arrowParens: 'always',
11 | trailingComma: 'none',
12 | importOrder: ['^node:.*', '', '^@/', '^[./]'],
13 | importOrderSeparation: true,
14 | importOrderSortSpecifiers: true,
15 | importOrderGroupNamespaceSpecifiers: true
16 | }
17 |
--------------------------------------------------------------------------------
/components/ActiveLink/ActiveLink.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import cs from 'clsx'
5 | import Link, { LinkProps } from 'next/link'
6 | import { usePathname } from 'next/navigation'
7 |
8 | type ActiveLinkProps = LinkProps & {
9 | children?: React.ReactNode
10 | className?: string
11 | activeClassName?: string
12 | style?: React.CSSProperties
13 |
14 | // optional comparison function to normalize URLs before comparing
15 | compare?: (a?: any, b?: any) => boolean
16 | }
17 |
18 | /**
19 | * Link that will be disabled if the target `href` is the same as the current
20 | * route's pathname.
21 | */
22 | export const ActiveLink = React.forwardRef(function ActiveLink(
23 | {
24 | children,
25 | href,
26 | style,
27 | className,
28 | activeClassName,
29 | onClick,
30 | prefetch,
31 | compare = (a, b) => a === b,
32 | ...props
33 | }: ActiveLinkProps,
34 | ref
35 | ) {
36 | const pathname = usePathname()
37 | const [disabled, setDisabled] = React.useState(false)
38 |
39 | React.useEffect(() => {
40 | const linkPathname = new URL(href as string, location.href).pathname
41 |
42 | setDisabled(compare(linkPathname, pathname))
43 | }, [pathname, href, compare])
44 |
45 | const styleOverride = React.useMemo(
46 | () =>
47 | disabled
48 | ? {
49 | ...style,
50 | pointerEvents: 'none'
51 | }
52 | : style ?? {},
53 | [disabled, style]
54 | )
55 |
56 | const onClickOverride = React.useCallback(
57 | (event: any): void => {
58 | if (disabled) {
59 | event.preventDefault()
60 | return
61 | }
62 |
63 | if (onClick) {
64 | onClick(event)
65 | return
66 | }
67 | },
68 | [disabled, onClick]
69 | )
70 |
71 | return (
72 |
81 | {children}
82 |
83 | )
84 | })
85 |
--------------------------------------------------------------------------------
/components/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import cs from 'clsx'
3 |
4 | import styles from './styles.module.css'
5 |
6 | export const Button: React.FC<
7 | {
8 | className?: string
9 | buttonClassName?: string
10 | children: React.ReactNode
11 | isLoading?: boolean
12 | ref?: any
13 | } & React.AnchorHTMLAttributes
14 | > = React.forwardRef(function Button(
15 | { className, buttonClassName, children, style, isLoading, ...buttonProps },
16 | ref
17 | ) {
18 | return (
19 |
28 | )
29 | })
30 |
--------------------------------------------------------------------------------
/components/Button/styles.module.css:
--------------------------------------------------------------------------------
1 | .buttonWrapper {
2 | position: relative;
3 | }
4 |
5 | .button {
6 | position: relative;
7 | cursor: pointer;
8 |
9 | background: var(--fg-color);
10 | color: var(--bg-color);
11 |
12 | border: 1px solid transparent;
13 | box-shadow: 0 4px 4px 0 #00000010;
14 | transition-property: color, background-color, box-shadow;
15 | transition-duration: 0.15s;
16 | transition-timing-function: ease;
17 | padding: 12px 24px;
18 | line-height: 1.5em;
19 | border-radius: 5px;
20 | max-width: 100%;
21 | font-weight: 400;
22 | font-size: 1rem;
23 | display: flex;
24 | justify-content: center;
25 | align-items: center;
26 | user-select: none;
27 | outline: none;
28 | }
29 |
30 | .buttonContent {
31 | text-overflow: ellipsis;
32 | white-space: nowrap;
33 | overflow: hidden;
34 | display: inline-block;
35 | }
36 |
37 | .button:hover,
38 | .button:focus {
39 | border-color: var(--fg-color);
40 | background-color: var(--bg-color);
41 | color: var(--fg-color);
42 | }
43 |
44 | .button:active {
45 | background-color: var(--bg-color-1);
46 | }
47 |
48 | .buttonWrapper:has(.button:disabled) {
49 | opacity: 0.3;
50 | cursor: not-allowed;
51 | }
52 |
53 | .buttonWrapper:has(.button:disabled) * {
54 | pointer-events: none;
55 | }
56 |
--------------------------------------------------------------------------------
/components/Footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import cs from 'clsx'
5 | import { useGlitch } from 'react-powerglitch'
6 |
7 | import * as config from '@/lib/config'
8 | import { Discord, GitHub, Twitter } from '@/icons/index'
9 |
10 | import styles from './styles.module.css'
11 |
12 | export const Footer: React.FC<{ className?: string }> = ({ className }) => {
13 | const glitch = useGlitch({ playMode: 'hover' })
14 |
15 | return (
16 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/components/Footer/styles.module.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | width: 100%;
3 | display: flex;
4 | flex-direction: row;
5 | justify-content: space-between;
6 | align-items: center;
7 | gap: 1rem;
8 | }
9 |
10 | .settings,
11 | .copyright,
12 | .social {
13 | flex: 1;
14 | }
15 |
16 | .settings {
17 | justify-content: center;
18 | }
19 |
20 | .social {
21 | justify-content: flex-end;
22 | }
23 |
24 | .copyright {
25 | font-size: 80%;
26 | padding: 0.5rem 0;
27 | }
28 |
29 | .settings,
30 | .social {
31 | display: flex;
32 | flex-direction: row;
33 | align-items: center;
34 | gap: 1rem;
35 | user-select: none;
36 | }
37 |
38 | .action {
39 | cursor: pointer;
40 | width: 2rem;
41 | height: 2rem;
42 | display: inline-flex;
43 | padding: 0.25rem;
44 | transition: color 250ms ease-out;
45 | }
46 |
47 | .footer .darkModeToggle {
48 | width: 1.75em;
49 | height: 1.75em;
50 | }
51 |
52 | .action:hover {
53 | transition: color 50ms ease-out;
54 | }
55 |
56 | .twitter:hover {
57 | color: #2795e9;
58 | }
59 |
60 | .github:hover {
61 | color: #c9510c;
62 | }
63 |
64 | .discord:hover {
65 | color: #5766f2;
66 | }
67 |
68 | @media only screen and (max-width: 500px) {
69 | .footer {
70 | flex-direction: column;
71 | }
72 |
73 | .footer .settings {
74 | justify-content: center;
75 | order: 1;
76 | }
77 |
78 | .footer .social {
79 | justify-content: center;
80 | order: 2;
81 | }
82 |
83 | .footer .copyright {
84 | justify-content: center;
85 | order: 3;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/components/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import cs from 'clsx'
3 | import Link from 'next/link'
4 | import { useGlitch } from 'react-powerglitch'
5 |
6 | import * as config from '@/lib/config'
7 | import { ActiveLink } from '@/components/ActiveLink/ActiveLink'
8 | import { Discord, GitHub, Twitter } from '@/icons/index'
9 |
10 | import { Logo } from './Logo'
11 | import styles from './styles.module.css'
12 |
13 | export const Header: React.FC<{ className?: string }> = ({ className }) => {
14 | const glitch = useGlitch({
15 | playMode: 'hover'
16 | })
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
65 |
66 |
67 | )
68 | }
69 |
--------------------------------------------------------------------------------
/components/Header/Logo.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import cs from 'clsx'
5 | import Image from 'next/image'
6 | import { useGlitch } from 'react-powerglitch'
7 |
8 | import LogoDark from '@/public/logo-dark.png'
9 |
10 | import styles from './styles.module.css'
11 |
12 | export const Logo: React.FC<{ className?: string }> = ({ className }) => {
13 | const glitch = useGlitch({
14 | playMode: 'hover'
15 | })
16 |
17 | return (
18 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/components/Header/styles.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | position: fixed;
3 | top: 0;
4 | left: 0;
5 | z-index: 200;
6 |
7 | width: 100%;
8 | max-width: 100vw;
9 | overflow: hidden;
10 | height: 72px;
11 | min-height: 72px;
12 | padding: 12px 0;
13 |
14 | background: hsla(0, 0%, 100%, 0.8);
15 | backdrop-filter: saturate(180%) blur(16px);
16 | }
17 |
18 | :global(.dark) .header {
19 | background: transparent;
20 | box-shadow: inset 0 -1px 0 0 rgba(0, 0, 0, 0.1);
21 | backdrop-filter: saturate(180%) blur(8px);
22 | }
23 |
24 | .navHeader {
25 | display: flex;
26 | flex-direction: row;
27 | justify-content: space-between;
28 | align-items: center;
29 | gap: var(--gap-w-1);
30 |
31 | max-width: var(--max-width);
32 | height: 100%;
33 | margin: 0 auto;
34 | }
35 |
36 | .rhs {
37 | display: flex;
38 | flex-direction: row;
39 | justify-content: flex-end;
40 | align-items: center;
41 | height: 100%;
42 | gap: var(--gap-w-1);
43 | }
44 |
45 | .social {
46 | cursor: pointer;
47 | width: 2rem;
48 | height: 2rem;
49 | display: inline-flex;
50 | padding: 0.25rem;
51 | transition: color 250ms ease-out;
52 | }
53 |
54 | .social:hover {
55 | transition: color 50ms ease-out;
56 | }
57 |
58 | .action {
59 | display: inline-flex;
60 | align-items: center;
61 | padding: 12px;
62 | line-height: 1;
63 | font-size: 14px;
64 | border-radius: 3px;
65 | white-space: nowrap;
66 | text-overflow: ellipsis;
67 | background-color: transparent;
68 | cursor: pointer;
69 | outline: none;
70 | transition:
71 | backgrond-color 200ms ease-in,
72 | color 200ms ease-in;
73 | }
74 |
75 | .icon {
76 | box-sizing: border-box;
77 | padding: 8px !important;
78 | width: calc(1rem + 24px);
79 | height: calc(1rem + 24px);
80 | }
81 |
82 | .icon > svg {
83 | width: 100%;
84 | height: 100%;
85 | }
86 |
87 | .action:not(.active):hover,
88 | .action:not(.active):focus {
89 | transition:
90 | backgrond-color 50ms ease-in,
91 | color 50ms ease-in;
92 | background-color: var(--bg-color-0);
93 | }
94 |
95 | .action:not(.active):active {
96 | background-color: var(--bg-color-1);
97 | }
98 |
99 | .action.active {
100 | cursor: default;
101 | }
102 |
103 | .twitter:hover {
104 | color: #2795e9;
105 | }
106 |
107 | .github:hover {
108 | color: #c9510c;
109 | }
110 |
111 | .discord:hover {
112 | color: #5766f2;
113 | }
114 |
115 | .logo {
116 | display: block;
117 | width: auto;
118 | max-width: 100%;
119 | height: 48px;
120 | object-fit: contain;
121 | }
122 |
123 | /* Workaround for Firefox not supporting backdrop-filter yet */
124 | /* @-moz-document url-prefix() {
125 | :global(.dark) .header {
126 | background: hsla(203, 8%, 20%, 0.8);
127 | }
128 | } */
129 |
--------------------------------------------------------------------------------
/components/HeroButton/HeroButton.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import cs from 'clsx'
3 |
4 | import styles from './styles.module.css'
5 |
6 | export type HeroButtonVariant = 'orange' | 'blue' | 'purple'
7 |
8 | export const HeroButton: React.FC<
9 | {
10 | variant?: HeroButtonVariant
11 | className?: string
12 | buttonClassName?: string
13 | children: React.ReactNode
14 | } & React.AnchorHTMLAttributes
15 | > = ({
16 | variant = 'purple',
17 | className,
18 | buttonClassName,
19 | children,
20 | style,
21 | ...buttonProps
22 | }) => {
23 | return (
24 |
25 | {variant === 'blue' && (
26 |
27 | )}
28 |
29 | {variant === 'purple' && (
30 |
31 | )}
32 |
33 | {variant === 'orange' && (
34 |
35 | )}
36 |
37 |
38 | {children}
39 |
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/components/HeroButton/styles.module.css:
--------------------------------------------------------------------------------
1 | .heroButtonWrapper {
2 | position: relative;
3 | }
4 |
5 | .heroButtonBg1 {
6 | --start-color: #00dfd8;
7 | --end-color: #007cf0;
8 | /* animation: heroBgAnimation1 8s infinite; */
9 | }
10 |
11 | .heroButtonBg2 {
12 | --start-color: #ff0080;
13 | --end-color: #7928ca;
14 | /* animation: heroBgAnimation2 8s infinite; */
15 | }
16 |
17 | .heroButtonBg3 {
18 | --start-color: #ff4d4d;
19 | --end-color: #f9cb28;
20 | /* animation: heroBgAnimation3 8s infinite; */
21 | }
22 |
23 | .heroButtonBg {
24 | position: absolute;
25 | width: 100%;
26 | height: 100%;
27 | background-image: linear-gradient(
28 | 165deg,
29 | var(--start-color),
30 | var(--end-color)
31 | );
32 | border-radius: 5px;
33 | z-index: -2;
34 | }
35 |
36 | .heroButtonBg::before {
37 | position: absolute;
38 | content: '';
39 | top: 0;
40 | left: 0;
41 | right: 0;
42 | bottom: 0;
43 | z-index: -1;
44 | border: 12px solid transparent;
45 | filter: blur(24px);
46 | /* animation: pulse 2s ease-in-out infinite alternate; */
47 | background-image: linear-gradient(
48 | 165deg,
49 | var(--start-color),
50 | var(--end-color)
51 | );
52 | }
53 |
54 | .heroButton {
55 | position: relative;
56 | cursor: pointer;
57 |
58 | background-color: var(--bg-color);
59 | background-clip: padding-box;
60 | border: 1px solid transparent;
61 | box-shadow: 0 4px 4px 0 #00000010;
62 | color: var(--fg-color);
63 | transition-property: color, background-color, box-shadow;
64 | transition-duration: 0.15s;
65 | transition-timing-function: ease;
66 | padding: 12px 24px;
67 | line-height: 1.5em;
68 | border-radius: 5px;
69 | max-width: 100%;
70 | font-weight: 400;
71 | font-size: 1rem;
72 | display: flex;
73 | justify-content: center;
74 | align-items: center;
75 | user-select: none;
76 | outline: none;
77 |
78 | --lighten-color: hsla(0, 0%, 100%, 0.8);
79 | background-image: linear-gradient(
80 | to right,
81 | var(--lighten-color),
82 | var(--lighten-color)
83 | );
84 | }
85 |
86 | :global(.dark) .heroButton {
87 | --lighten-color: rgba(0, 0, 0, 0.75);
88 | }
89 |
90 | .heroButtonContent {
91 | text-overflow: ellipsis;
92 | white-space: nowrap;
93 | overflow: hidden;
94 | display: inline-block;
95 | }
96 |
97 | .heroButton:hover {
98 | --lighten-color: transparent;
99 | background-color: transparent;
100 | color: var(--bg-color);
101 | }
102 |
103 | .heroButton:focus:not(:active):not(:hover) {
104 | border-color: var(--fg-color);
105 | }
106 |
107 | .heroButton:active {
108 | --lighten-color: hsla(0, 0%, 100%, 0.5);
109 | }
110 |
111 | .heroButtonWrapper:has(.heroButton:disabled) {
112 | opacity: 0.3;
113 | cursor: not-allowed;
114 | }
115 |
116 | .heroButtonWrapper:has(.heroButton:disabled) * {
117 | pointer-events: none;
118 | }
119 |
120 | /* @keyframes heroBgAnimation1 {
121 | 0%,
122 | 16.667%,
123 | to {
124 | opacity: 1;
125 | }
126 | 33%,
127 | 83.333% {
128 | opacity: 0;
129 | }
130 | }
131 |
132 | @keyframes heroBgAnimation2 {
133 | 0%,
134 | 16.667%,
135 | 66.667%,
136 | to {
137 | opacity: 0;
138 | }
139 | 33.333%,
140 | 50% {
141 | opacity: 1;
142 | }
143 | }
144 |
145 | @keyframes heroBgAnimation3 {
146 | 0%,
147 | 50%,
148 | to {
149 | opacity: 0;
150 | }
151 | 66.667%,
152 | 83.333% {
153 | opacity: 1;
154 | }
155 | } */
156 |
157 | /* @keyframes pulse {
158 | from {
159 | filter: blur(8px);
160 | }
161 |
162 | to {
163 | filter: blur(32px);
164 | }
165 | } */
166 |
--------------------------------------------------------------------------------
/components/Layout/Layout.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import cs from 'clsx'
3 | import { Inter } from 'next/font/google'
4 |
5 | import { Footer } from '@/components/Footer/Footer'
6 | import { Header } from '@/components/Header/Header'
7 |
8 | import { RootLayoutProviders } from './providers'
9 | import styles from './styles.module.css'
10 |
11 | const inter = Inter({ subsets: ['latin'] })
12 |
13 | export function Layout({ children }: { children: React.ReactNode }) {
14 | return (
15 |
16 |
17 |
18 |
19 | {children}
20 |
21 |
22 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/components/Layout/providers.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { MotionConfig } from 'framer-motion'
5 |
6 | export function RootLayoutProviders({
7 | children
8 | }: {
9 | children: React.ReactNode
10 | }) {
11 | return {children}
12 | }
13 |
--------------------------------------------------------------------------------
/components/Layout/styles.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | flex: 1;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: space-around;
6 | align-items: center;
7 | margin: 0 auto;
8 | max-width: var(--max-width);
9 | min-height: 100vh;
10 | padding: 0;
11 | }
12 |
13 | .header {
14 | padding: 12px 12px 0;
15 | }
16 |
17 | .footer {
18 | padding: 0 12px 12px;
19 | }
20 |
21 | .main {
22 | flex: 1;
23 | width: 100%;
24 | max-width: var(--max-body-width);
25 | display: flex;
26 | flex-direction: column;
27 | align-items: center;
28 | margin-bottom: var(--gap-h);
29 | padding: 0 12px;
30 | }
31 |
32 | @media (max-width: 800px) {
33 | .header {
34 | padding: 0;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/components/Markdown/Markdown.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import cs from 'clsx'
3 |
4 | import styles from './styles.module.css'
5 |
6 | /**
7 | * `content` is assumed to have already been transformed into HTML via remark/rehype.
8 | */
9 | export const Markdown: React.FC<{ className?: string; content: string }> = ({
10 | className,
11 | content
12 | }) => {
13 | return (
14 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/components/Markdown/styles.module.css:
--------------------------------------------------------------------------------
1 | .markdown {
2 | max-width: 830px;
3 | line-height: 1.6;
4 | }
5 |
6 | .markdown > *:first-child {
7 | margin-top: 0 !important;
8 | }
9 |
10 | .markdown img {
11 | display: inline-block;
12 | max-width: 100%;
13 | margin: 0;
14 | }
15 |
16 | /* TODO: this is hacky */
17 | .markdown img[height] {
18 | height: 65px;
19 | }
20 |
21 | .markdown a:not(:has(img)) {
22 | /* color: #0969da;
23 | text-decoration: none;*/
24 | display: inline-block;
25 | text-decoration: none;
26 | font-weight: inherit;
27 | line-height: 1.3;
28 | position: relative;
29 | transition: unset;
30 | opacity: 1;
31 | color: unset;
32 | border-color: var(--fg-color-2);
33 | border-bottom-width: 0.135rem;
34 | background: transparent;
35 | background-origin: border-box;
36 | background-repeat: no-repeat;
37 | background-position: 50% 100%;
38 | background-size: 0 0.135rem;
39 | }
40 |
41 | /*
42 | .markdown a {
43 | color: #0969da;
44 | text-decoration: none;
45 |
46 | :global(.dark) .markdown a {
47 | color: #3291ff;
48 | }
49 |
50 | .markdown a:hover {
51 | text-decoration: underline;
52 | } */
53 |
54 | .markdown a:hover:not(:has(img)),
55 | .markdown a:focus:not(:has(img)) {
56 | color: var(--tw-prose-links);
57 | text-decoration: none;
58 | border-bottom-color: transparent;
59 |
60 | background-image: linear-gradient(90.68deg, #b439df 0.26%, #e5337e 102.37%);
61 | background-repeat: no-repeat;
62 | background-position: 0 100%;
63 | background-size: 100% 0.135rem;
64 |
65 | transition-property: background-position, background-size;
66 | transition-duration: 300ms;
67 | }
68 |
69 | .markdown p {
70 | margin-top: 0;
71 | margin-bottom: 1em;
72 | }
73 |
74 | .markdown li a {
75 | margin: 0;
76 | }
77 |
78 | .markdown h1,
79 | .markdown h2,
80 | .markdown h3,
81 | .markdown h4,
82 | .markdown h5,
83 | .markdown h6 {
84 | margin-top: 2em;
85 | margin-bottom: 0.5em;
86 | font-weight: 600;
87 | line-height: 1.25;
88 | }
89 |
90 | .markdown h1 {
91 | font-size: 2em;
92 | border-bottom: 1px solid var(--var-bg-2);
93 | }
94 |
95 | .markdown h2 {
96 | font-size: 1.5em;
97 | border-bottom: 1px solid var(--var-bg-2);
98 | }
99 |
100 | .markdown h3 {
101 | font-size: 1.25em;
102 | }
103 |
104 | .markdown h4 {
105 | font-size: 1em;
106 | }
107 |
108 | .markdown h5 {
109 | font-size: 1em;
110 | font-weight: 700;
111 | }
112 |
113 | .markdown h6 {
114 | font-size: 1em;
115 | font-weight: 500;
116 | }
117 |
118 | @media (max-width: 800px) {
119 | .markdown img {
120 | display: block;
121 | text-align: center;
122 | margin: 0 auto;
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/components/MetaballVisualization/Body.ts:
--------------------------------------------------------------------------------
1 | // scaling factor that affects the mass of the bodies, effectively scaling the
2 | // overall gravity strength of the system
3 | const GRAVITY_SPEED = 900
4 | const MAX_GRAVITY_SPEED = 1000
5 |
6 | export class Body {
7 | // position
8 | x: number
9 | y: number
10 |
11 | // velocity vector
12 | dx: number
13 | dy: number
14 |
15 | // affects this body's gravitational pull w.r.t. other bodies
16 | mass: number
17 | r: number
18 |
19 | visible: boolean
20 |
21 | constructor({
22 | x,
23 | y,
24 | dx = 0,
25 | dy = 0,
26 | r,
27 | visible = true
28 | }: {
29 | x: number
30 | y: number
31 | r: number
32 | dx?: number
33 | dy?: number
34 | visible?: boolean
35 | }) {
36 | this.x = x
37 | this.y = y
38 |
39 | this.dx = dx
40 | this.dy = dy
41 |
42 | this.r = r
43 | this.mass = (2 * this.r) / (MAX_GRAVITY_SPEED - GRAVITY_SPEED)
44 | this.visible = visible
45 | }
46 |
47 | addAcceleration(accelX: number, accelY: number) {
48 | this.dx += accelX
49 | this.dy += accelY
50 | }
51 |
52 | update() {
53 | if (!this.visible) {
54 | return
55 | }
56 |
57 | this.x += this.dx
58 | this.y += this.dy
59 | }
60 |
61 | // returns whether or not this body intersects the given body (currently unused)
62 | intersects(body: Body): boolean {
63 | const radius = this.r
64 | const rad = body.r
65 | const xDif = body.x - this.x
66 | const yDif = body.y - this.y
67 | const dist = Math.sqrt(xDif * xDif + yDif * yDif)
68 |
69 | // reject if dist btwn circles is greater than their radii combined
70 | if (dist > radius + rad) {
71 | return false
72 | }
73 |
74 | // reject if one circle is inside of the other
75 | return dist >= Math.abs(rad - radius)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/components/MetaballVisualization/MetaballVisualization.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useMeasure } from 'react-use'
3 |
4 | import { Metaballs } from '@/store/metaballs'
5 |
6 | import { MetaballViz } from './MetaballViz'
7 | import styles from './styles.module.css'
8 |
9 | export const MetaballVisualization: React.FC = () => {
10 | const { metaballVizRef } = Metaballs.useContainer()
11 | const canvasRef = React.useRef(null)
12 |
13 | const defaultWidth = typeof window !== 'undefined' ? window.innerWidth : 1280
14 | const defaultHeight = typeof window !== 'undefined' ? window.innerHeight : 720
15 | const [measureRef, { width = defaultWidth, height = defaultHeight }] =
16 | useMeasure()
17 |
18 | React.useEffect(() => {
19 | if (!canvasRef.current) return
20 |
21 | const metaballViz = new MetaballViz({
22 | canvas: canvasRef.current
23 | })
24 | metaballVizRef.current = metaballViz
25 | metaballViz.animate()
26 |
27 | return () => metaballViz.destroy()
28 | }, [canvasRef, metaballVizRef])
29 |
30 | React.useEffect(() => {
31 | if (!metaballVizRef?.current) return
32 | metaballVizRef.current.onResize()
33 | }, [width, height, metaballVizRef])
34 |
35 | const onMouseMove = React.useCallback(
36 | (event: any) => {
37 | if (!metaballVizRef?.current) return
38 | metaballVizRef.current.onMouseMove(event)
39 | canvasRef.current?.focus()
40 | },
41 | [metaballVizRef, canvasRef]
42 | )
43 |
44 | return (
45 |
46 |
53 |
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/components/MetaballVisualization/MetaballViz.ts:
--------------------------------------------------------------------------------
1 | import random from 'random'
2 | import {
3 | Mesh,
4 | OrthographicCamera,
5 | PlaneGeometry,
6 | Scene,
7 | ShaderMaterial,
8 | Vector2,
9 | WebGLRenderer
10 | } from 'three'
11 |
12 | import { Body } from './Body'
13 |
14 | // https://www.shadertoy.com/view/XssSzN
15 | // https://blog.hyuntak.com/metaball
16 |
17 | export class MetaballViz {
18 | _canvas: HTMLCanvasElement
19 | _renderer: WebGLRenderer
20 | _scene: Scene
21 | _camera: OrthographicCamera
22 | _material: ShaderMaterial
23 |
24 | _metaballs: Body[]
25 | _metaballsData: Float32Array
26 | _numMetaballs: number
27 |
28 | _rafHandle?: number | null
29 | // _mouse?: Vector2
30 |
31 | constructor({
32 | canvas,
33 | numMetaballs = 50
34 | }: {
35 | canvas: HTMLCanvasElement
36 | numMetaballs?: number
37 | }) {
38 | this._canvas = canvas
39 | this._numMetaballs = numMetaballs
40 |
41 | const { width, height } = this._canvas
42 |
43 | this._renderer = new WebGLRenderer({
44 | antialias: true,
45 | alpha: true,
46 | canvas: this._canvas
47 | })
48 | this._renderer.setSize(width, height)
49 | this._renderer.setClearColor(0x121212)
50 | // this._renderer.setPixelRatio(window.devicePixelRatio)
51 |
52 | this._camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1)
53 | this._scene = new Scene()
54 | const geometry = new PlaneGeometry(2, 2)
55 |
56 | const vertexShader = `
57 | void main() {
58 | gl_Position = vec4(position, 1.0);
59 | }
60 | `
61 |
62 | // const fragmentShader = `
63 | // uniform vec2 screen;
64 | // // uniform vec2 mouse;
65 | // uniform vec3 metaballs[${numMetaballs}];
66 |
67 | // void main(){
68 | // float x = gl_FragCoord.x;
69 | // float y = gl_FragCoord.y;
70 |
71 | // float sum = 0.0;
72 | // for (int i = 0; i < ${numMetaballs}; i++) {
73 | // vec3 metaball = metaballs[i];
74 | // float dx = metaball.x - x;
75 | // float dy = metaball.y - y;
76 | // float radius = metaball.z;
77 |
78 | // sum += (radius * radius) / (dx * dx + dy * dy);
79 | // // sum += pow((radius * radius) / (dx * dx + dy * dy), 0.8);
80 | // }
81 |
82 | // // if (mouse.x > 0.0 && mouse.y > 0.0) {
83 | // // float dx = mouse.x - x;
84 | // // float dy = (screen.y - mouse.y) - y;
85 | // // float radius = 50.0;
86 | // // sum -= (radius * radius) / (dx * dx + dy * dy);
87 | // // }
88 |
89 | // if (sum >= 0.99) {
90 | // gl_FragColor = vec4(mix(vec3(x / screen.x, y / screen.y, 1.0), vec3(0, 0, 0), max(0.0, 1.0 - (sum - 0.99) * 100.0)), 1.0);
91 | // return;
92 | // }
93 |
94 | // discard;
95 | // }
96 | // `
97 |
98 | const fragmentShader = `
99 | uniform vec2 screen;
100 | uniform vec3 metaballs[${numMetaballs}];
101 |
102 | void main(){
103 | float x = gl_FragCoord.x;
104 | float y = gl_FragCoord.y;
105 |
106 | float sum = 0.0;
107 | for (int i = 0; i < ${numMetaballs}; i++) {
108 | vec3 metaball = metaballs[i];
109 | float dx = metaball.x - x;
110 | float dy = metaball.y - y;
111 | float radius = metaball.z;
112 |
113 | sum += (radius * radius) / (dx * dx + dy * dy);
114 | }
115 |
116 | if (sum >= 0.99) {
117 | gl_FragColor = vec4(mix(vec3(x / screen.x, y / screen.y, 1.0), vec3(0, 0, 0), max(0.0, 1.0 - (sum - 0.99) * 100.0)), 1.0);
118 | } else {
119 | gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
120 | }
121 | }
122 |
123 | `
124 |
125 | this._metaballs = []
126 | this._metaballsData = new Float32Array(3 * this._numMetaballs)
127 |
128 | this._resetMetaballs()
129 | this.update()
130 |
131 | this._material = new ShaderMaterial({
132 | uniforms: {
133 | screen: {
134 | value: new Vector2(width, height)
135 | },
136 | // mouse: {
137 | // value: new Vector2(0, 0)
138 | // },
139 | metaballs: {
140 | value: this._metaballsData
141 | }
142 | },
143 | vertexShader,
144 | fragmentShader
145 | // transparent: true
146 | })
147 |
148 | const mesh = new Mesh(geometry, this._material)
149 | this._scene.add(mesh)
150 | }
151 |
152 | _resetMetaballs() {
153 | const { width, height } = this._canvas
154 |
155 | const x0 = width / 2
156 | const y0 = height / 2
157 |
158 | this._metaballs = []
159 | this._metaballs.push(
160 | new Body({
161 | x: x0,
162 | y: y0,
163 | r: 500,
164 | dx: 0,
165 | dy: 0,
166 | visible: false
167 | })
168 | )
169 |
170 | const r0 = Math.sqrt(width * width + height * height) / 10
171 |
172 | for (let i = 1; i < this._numMetaballs + 1; i++) {
173 | const theta = ((i - 1) * 2.0 * Math.PI) / this._numMetaballs
174 | const x = x0 + r0 * Math.cos(theta)
175 | const y = y0 + r0 * Math.sin(theta)
176 | const dx = 0
177 | const dy = 0
178 | const r = random.float(10, 45)
179 |
180 | // const r = random.float(10, 70) * 0.75
181 | // const x = random.float(r, width - r)
182 | // const y = random.float(r, height - r)
183 | // const dx = random.float(-1.5, 1.5)
184 | // const dy = random.float(-1.5, 1.5)
185 |
186 | this._metaballs.push(
187 | new Body({
188 | x,
189 | y,
190 | dx,
191 | dy,
192 | r
193 | })
194 | )
195 | }
196 | }
197 |
198 | destroy() {
199 | this.pause()
200 | }
201 |
202 | pause() {
203 | if (this._rafHandle) {
204 | cancelAnimationFrame(this._rafHandle)
205 | this._rafHandle = null
206 | }
207 | }
208 |
209 | animate() {
210 | this.update()
211 | this.render()
212 |
213 | this._rafHandle = requestAnimationFrame(this.animate.bind(this))
214 | }
215 |
216 | onResize = () => {
217 | const { width, height } = this._canvas
218 |
219 | this._resetMetaballs()
220 |
221 | this._renderer.setSize(width, height)
222 | this._material.uniforms.screen.value = new Vector2(width, height)
223 | }
224 |
225 | update() {
226 | const width = this._canvas.width
227 | const height = this._canvas.height
228 |
229 | // const maxInvDist = 0.0001
230 | const maxInvDist = 10.0
231 |
232 | // for each body in the system, enact a force on every other body in the system
233 | // running time O(num_particles ^ 2)
234 | for (let i = 0; i < this._numMetaballs + 1; i++) {
235 | const mi = this._metaballs[i]
236 |
237 | for (let j = i + 1; j < this._numMetaballs + 1; j++) {
238 | const mj = this._metaballs[j]
239 | let xDif = mj.x - mi.x
240 | let yDif = mj.y - mi.y
241 | const invDistSquared = Math.min(
242 | 1.0 / (xDif * xDif + yDif * yDif),
243 | maxInvDist
244 | )
245 | if (xDif === 0 || yDif === 0) {
246 | continue
247 | }
248 |
249 | // force is inversely proportional to the distance squared
250 | xDif *= invDistSquared
251 | yDif *= invDistSquared
252 |
253 | mi.addAcceleration(xDif * mj.mass, yDif * mj.mass)
254 | mj.addAcceleration(-xDif * mi.mass, -yDif * mi.mass)
255 | }
256 |
257 | // if (this._mouse) {
258 | // const mj = this._mouse
259 | // let xDif = mj.x - mi.x
260 | // let yDif = height - mj.y - mi.y
261 | // const invDistSquared = 1.0 / (xDif * xDif + yDif * yDif)
262 | // if (xDif === 0 || yDif === 0) {
263 | // continue
264 | // }
265 |
266 | // xDif *= invDistSquared
267 | // yDif *= invDistSquared
268 |
269 | // mi.addAcceleration(-xDif * 20, -yDif * 20)
270 | // }
271 | }
272 |
273 | const maxV = 10
274 | // let maxDx = 0
275 | // let maxDy = 0
276 |
277 | for (let i = 1; i < this._numMetaballs + 1; i++) {
278 | const metaball = this._metaballs[i]
279 | metaball.update()
280 |
281 | if (metaball.x < metaball.r) {
282 | metaball.dx = Math.abs(metaball.dx)
283 | }
284 | if (metaball.x > width - metaball.r) {
285 | metaball.dx = -Math.abs(metaball.dx)
286 | }
287 |
288 | if (metaball.y < metaball.r) {
289 | metaball.dy = Math.abs(metaball.dy)
290 | }
291 |
292 | if (metaball.y > height - metaball.r) {
293 | metaball.dy = -Math.abs(metaball.dy)
294 | }
295 |
296 | if (metaball.dx > maxV) {
297 | metaball.dx = maxV
298 | }
299 |
300 | if (metaball.dx < -maxV) {
301 | metaball.dx = -maxV
302 | }
303 |
304 | if (metaball.dy > maxV) {
305 | metaball.dy = maxV
306 | }
307 |
308 | if (metaball.dy < -maxV) {
309 | metaball.dy = -maxV
310 | }
311 |
312 | // if (Math.abs(metaball.dx) > maxDx) {
313 | // maxDx = Math.abs(metaball.dx)
314 | // }
315 |
316 | // if (Math.abs(metaball.dy) > maxDy) {
317 | // maxDy = Math.abs(metaball.dy)
318 | // }
319 |
320 | const baseIndex = 3 * (i - 1)
321 | this._metaballsData[baseIndex + 0] = metaball.x
322 | this._metaballsData[baseIndex + 1] = metaball.y
323 | this._metaballsData[baseIndex + 2] = metaball.r
324 | }
325 | }
326 |
327 | onMouseMove = (event: any) => {
328 | // const { clientX, clientY } = this.getEventCoordinates(event)
329 | // this._mouse = new Vector2(clientX, clientY)
330 | // console.log({ clientX, clientY })
331 | // this._material.uniforms.mouse.value = new Vector2(clientX, clientY)
332 | }
333 |
334 | getEventCoordinates(event: any) {
335 | let clientX = 0
336 | let clientY = 0
337 |
338 | if (event.type === 'touchmove' || event.type === 'touchstart') {
339 | clientX = event.touches[0].clientX
340 | clientY = event.touches[0].clientY
341 | } else {
342 | clientX = event.clientX
343 | clientY = event.clientY
344 | }
345 |
346 | return { clientX, clientY }
347 | }
348 |
349 | render() {
350 | this._material.uniforms.metaballs.value = this._metaballsData
351 | this._material.uniformsNeedUpdate = true
352 |
353 | this._renderer.render(this._scene, this._camera)
354 | }
355 | }
356 |
--------------------------------------------------------------------------------
/components/MetaballVisualization/styles.module.css:
--------------------------------------------------------------------------------
1 | .metaballVisualization {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | width: 100%;
6 | height: 100vh;
7 | min-height: 480px;
8 | max-height: 4096px;
9 | }
10 |
11 | .metaballVisualization canvas {
12 | position: absolute;
13 | top: 0;
14 | left: 0;
15 | user-select: none;
16 | outline: none;
17 | width: 100%;
18 | height: 100%;
19 | max-width: 100vw;
20 | max-height: 100vh;
21 | }
22 |
--------------------------------------------------------------------------------
/components/PageHead/PageHead.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import Head from 'next/head'
3 |
4 | import * as config from '@/lib/config'
5 |
6 | export const PageHead: React.FC<{
7 | title?: string
8 | description?: string
9 | imagePathname?: string
10 | pathname?: string
11 | }> = ({
12 | title = config.title,
13 | description = config.description,
14 | imagePathname,
15 | pathname
16 | }) => {
17 | const url = pathname ? `${config.url}${pathname}` : config.url
18 | const imageUrl = imagePathname
19 | ? `${config.url}${imagePathname}`
20 | : config.socialImageUrl
21 |
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {description && (
33 | <>
34 |
35 |
36 |
37 | >
38 | )}
39 |
40 | {imageUrl ? (
41 | <>
42 |
43 |
44 |
45 | >
46 | ) : (
47 |
48 | )}
49 |
50 | {url && (
51 | <>
52 |
53 |
54 |
55 | >
56 | )}
57 |
58 |
59 |
60 | {title}
61 |
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/components/WebGLSupportChecker/WebGLSupportChecker.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react'
2 |
3 | interface WebGLSupportCheckerProps {
4 | children: ReactNode
5 | fallback: ReactNode
6 | }
7 |
8 | const WebGLSupportChecker: React.FC = ({
9 | children,
10 | fallback
11 | }) => {
12 | const isWebGLSupported = () => {
13 | try {
14 | const canvas = document.createElement('canvas')
15 | return !!(
16 | window.WebGLRenderingContext &&
17 | (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))
18 | )
19 | } catch (e) {
20 | return false
21 | }
22 | }
23 |
24 | return isWebGLSupported() ? <>{children}> : <>{fallback}>
25 | }
26 |
27 | export default WebGLSupportChecker
28 |
--------------------------------------------------------------------------------
/icons/Discord.tsx:
--------------------------------------------------------------------------------
1 | export const Discord = ({ className }: { className?: string }) => (
2 | //
14 |
15 |
30 | )
31 |
--------------------------------------------------------------------------------
/icons/GitHub.tsx:
--------------------------------------------------------------------------------
1 | export const GitHub = ({ className }: { className?: string }) => (
2 |
5 | )
6 |
--------------------------------------------------------------------------------
/icons/Twitter.tsx:
--------------------------------------------------------------------------------
1 | export const Twitter = ({ className }: { className?: string }) => (
2 |
5 | )
6 |
--------------------------------------------------------------------------------
/icons/index.ts:
--------------------------------------------------------------------------------
1 | export * from './GitHub'
2 | export * from './Discord'
3 | export * from './Twitter'
4 |
--------------------------------------------------------------------------------
/lib/bootstrap.ts:
--------------------------------------------------------------------------------
1 | import { githubRepoUrl, isSafari, isServer } from './config'
2 |
3 | const detail = `This webapp is open source: ${githubRepoUrl}`
4 | const banner = `
5 |
6 | ████████╗██████╗ █████╗ ███╗ ██╗███████╗██╗████████╗██╗██╗ ██╗███████╗ ██████╗ ███████╗
7 | ╚══██╔══╝██╔══██╗██╔══██╗████╗ ██║██╔════╝██║╚══██╔══╝██║██║ ██║██╔════╝ ██╔══██╗██╔════╝
8 | ██║ ██████╔╝███████║██╔██╗ ██║███████╗██║ ██║ ██║██║ ██║█████╗ ██████╔╝███████╗
9 | ██║ ██╔══██╗██╔══██║██║╚██╗██║╚════██║██║ ██║ ██║╚██╗ ██╔╝██╔══╝ ██╔══██╗╚════██║
10 | ██║ ██║ ██║██║ ██║██║ ╚████║███████║██║ ██║ ██║ ╚████╔╝ ███████╗ ██████╔╝███████║
11 | ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═══╝ ╚══════╝ ╚═════╝ ╚══════╝
12 |
13 | ${detail}
14 | `
15 |
16 | export async function bootstrap() {
17 | if (isServer) return
18 |
19 | if (isSafari) {
20 | console.log(detail)
21 | } else {
22 | console.log(banner)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/lib/config.ts:
--------------------------------------------------------------------------------
1 | export const environment = process.env.NODE_ENV || 'development'
2 | export const isDev = environment === 'development'
3 | export const isServer = typeof window === 'undefined'
4 | export const isSafari =
5 | !isServer && /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
6 |
7 | export const title = 'ChatGPT Hackers'
8 | export const description =
9 | 'Join thousands of other developers, researchers, and AI enthusiasts who are building at the cutting edge of AI!'
10 | export const domain = 'www.chatgpthackers.dev'
11 |
12 | export const author = 'Travis Fischer'
13 | export const twitter = 'transitive_bs'
14 | export const twitterUrl = `https://twitter.com/${twitter}`
15 | export const discordUrl = 'https://discord.gg/v9gERj825w'
16 | export const githubRepoUrl =
17 | 'https://github.com/transitive-bullshit/chatgpt-hackers'
18 | export const githubSponsorsUrl =
19 | 'https://github.com/sponsors/transitive-bullshit'
20 | export const copyright = `Copyright 2023 ${author}`
21 | export const madeWithLove = 'Made with ❤️ in Brooklyn, NY'
22 |
23 | export const port = process.env.PORT || '3000'
24 | export const prodUrl = `https://${domain}`
25 | export const url = isDev ? `http://localhost:${port}` : prodUrl
26 |
27 | export const apiBaseUrl =
28 | isDev || !process.env.VERCEL_URL ? url : `https://${process.env.VERCEL_URL}`
29 |
30 | // these must all be absolute urls
31 | export const socialImageUrl = `${url}/social.jpg`
32 |
--------------------------------------------------------------------------------
/lib/markdown-to-html.ts:
--------------------------------------------------------------------------------
1 | import rehypeFormat from 'rehype-format'
2 | import rehypeRaw from 'rehype-raw'
3 | import rehypeStringify from 'rehype-stringify'
4 | import remarkGfm from 'remark-gfm'
5 | import remarkParse from 'remark-parse'
6 | import remarkRehype from 'remark-rehype'
7 | import { unified } from 'unified'
8 |
9 | const processor = unified()
10 | .use(remarkParse)
11 | .use(remarkGfm)
12 | .use(remarkRehype, {
13 | allowDangerousHtml: true
14 | })
15 | .use(rehypeRaw)
16 | .use(rehypeFormat)
17 | .use(rehypeStringify)
18 |
19 | export async function markdownToHtml(markdown: string) {
20 | const result = await processor.process(markdown)
21 | return result.toString()
22 | }
23 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Travis Fischer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true
4 | }
5 |
6 | export default nextConfig
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chatgpt-hackers",
3 | "version": "0.1.0",
4 | "private": true,
5 | "description": "Website for the ChatGPT Hackers community.",
6 | "author": "Travis Fischer ",
7 | "repository": "transitive-bullshit/chatgpt-hackers",
8 | "homepage": "https://www.chatgpthackers.dev",
9 | "license": "MIT",
10 | "engines": {
11 | "node": ">=16"
12 | },
13 | "scripts": {
14 | "dev": "next dev",
15 | "build": "next build",
16 | "start": "next start",
17 | "prepare": "husky install",
18 | "pre-commit": "lint-staged",
19 | "test": "run-p test:*",
20 | "test:lint": "next lint",
21 | "test:prettier": "prettier '**/*.{js,jsx,ts,tsx}' --check"
22 | },
23 | "dependencies": {
24 | "clsx": "^1.2.1",
25 | "framer-motion": "^10.10.0",
26 | "human-number": "^2.0.1",
27 | "next": "13.2.4",
28 | "raf": "^3.4.1",
29 | "random": "^4.1.0",
30 | "react": "18.2.0",
31 | "react-dom": "18.2.0",
32 | "react-powerglitch": "^1.0.0",
33 | "react-use": "^17.4.0",
34 | "rehype-format": "^4.0.1",
35 | "rehype-raw": "^6.1.1",
36 | "rehype-stringify": "^9.0.3",
37 | "remark-gfm": "^3.0.1",
38 | "remark-parse": "^10.0.1",
39 | "remark-rehype": "^10.1.0",
40 | "three": "^0.151.3",
41 | "unified": "^10.1.2",
42 | "unstated-next": "^1.1.0"
43 | },
44 | "devDependencies": {
45 | "@tailwindcss/typography": "^0.5.9",
46 | "@trivago/prettier-plugin-sort-imports": "^4.1.1",
47 | "@types/node": "18.15.11",
48 | "@types/react": "18.0.33",
49 | "@types/react-dom": "18.0.11",
50 | "@types/three": "^0.150.1",
51 | "@vercel/analytics": "^0.1.11",
52 | "autoprefixer": "^10.4.14",
53 | "eslint": "8.37.0",
54 | "eslint-config-next": "13.2.4",
55 | "husky": "^8.0.3",
56 | "lint-staged": "^13.2.0",
57 | "npm-run-all": "^4.1.5",
58 | "postcss": "^8.4.21",
59 | "postcss-import": "^15.1.0",
60 | "prettier": "^2.8.7",
61 | "tailwindcss": "^3.3.1",
62 | "typescript": "5.0.3"
63 | },
64 | "lint-staged": {
65 | "*.{ts,tsx}": [
66 | "prettier --write"
67 | ]
68 | },
69 | "keywords": [
70 | "chatgpt",
71 | "openai",
72 | "ai",
73 | "hackers",
74 | "hacking",
75 | "gpt",
76 | "gpt-3",
77 | "gpt3",
78 | "gpt4",
79 | "gpt-4",
80 | "chatbot",
81 | "machine learning",
82 | "ml",
83 | "bot",
84 | "oss",
85 | "open source"
86 | ]
87 | }
88 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Analytics } from '@vercel/analytics/react'
3 | import type { AppProps } from 'next/app'
4 | import Head from 'next/head'
5 |
6 | import { bootstrap } from '@/lib/bootstrap'
7 | import { isServer } from '@/lib/config'
8 | import '@/styles/globals.css'
9 |
10 | if (!isServer) {
11 | bootstrap()
12 | }
13 |
14 | export default function App({
15 | Component,
16 | pageProps: { session, ...pageProps }
17 | }: AppProps) {
18 | return (
19 | <>
20 |
21 |
22 |
23 |
24 |
28 |
29 |
30 |
31 |
32 |
33 | >
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Head, Html, Main, NextScript } from 'next/document'
3 |
4 | export default function Document() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/pages/about/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { InferGetStaticPropsType } from 'next'
3 |
4 | import * as config from '@/lib/config'
5 | import { Layout } from '@/components/Layout/Layout'
6 | import { Markdown } from '@/components/Markdown/Markdown'
7 | import { markdownToHtml } from '@/lib/markdown-to-html'
8 |
9 | import styles from './styles.module.css'
10 |
11 | const markdownContent = `
12 | ## About
13 |
14 | When ChatGPT launched at the end of 2022, the world changed for some of us. Within a few days, we had built multiple open source ChatGPT API wrappers for [Python](https://github.com/acheong08/ChatGPT) and [Node.js](https://github.com/transitive-bullshit/chatgpt-api), and these projects quickly skyrocketed to the top of GitHub Trending.
15 |
16 | Since then, tens of thousands of developers have used our open source libraries to build amazing projects, bots, extensions, experiments, and products. We've honestly been blown away by the creativity and passion of the community, and so we decided to create a [Discord server](${config.discordUrl}) to help bring everyone together.
17 |
18 | Our community's grown rapidly over the past few months, and we now have **over 7000 members**. We're a group of developers, researchers, hackers, and AI enthusiasts who are all excited about building at the cutting edge of AI. Open source is also very much at the heart of what we do, and our community members are always building fun / crazy / useful OSS projects that push the boundaries of what's possible with the latest advances in AI.
19 |
20 | ## Admins
21 |
22 | - [Travis Fischer aka transitive-bullshit](https://twitter.com/transitive_bs) - [github](https://github.com/transitive-bullshit), [twitter](https://twitter.com/transitive_bs), [linkedin](https://www.linkedin.com/in/fisch2/)
23 | - [Antonio Cheong aka acheong08](https://twitter.com/GodlyIgnorance) - [github](https://github.com/acheong08), [twitter](https://twitter.com/GodlyIgnorance), [linkedin](https://www.linkedin.com/in/acheong08/)
24 | - [Joel Zhang aka waylaidwanderer](https://twitter.com/TheCodeOfJoel) - [github](https://github.com/waylaidwanderer), [twitter](https://twitter.com/TheCodeOfJoel), [linkedin](https://www.linkedin.com/in/joelczhang/)
25 | - [Rawand Ahmed Shaswar aka rawa](https://twitter.com/RawandShaswar) - [github](https://github.com/rawandahmad698), [twitter](https://twitter.com/RawandShaswar), [linkedin](https://www.linkedin.com/in/rawand-ahmed-shaswar-39a945215/)
26 |
27 | ## Selected OSS AI Projects
28 |
29 | Here's just a few of the amazing OSS projects that have been built by our community members. If you're a member of our community and you build something cool with AI, [create a PR](https://github.com/transitive-bullshit/chatgpt-hackers), and we'll add it to the list!
30 |
31 | ### JavaScript / TypeScript
32 |
33 | - [chatgpt](https://github.com/transitive-bullshit/chatgpt-api) [](https://github.com/transitive-bullshit/chatgpt-api)
34 | - Node.js client for the official ChatGPT API 🔥
35 | - Also supports the unofficial API
36 | - [node-chatgpt-api](https://github.com/waylaidwanderer/node-chatgpt-api) [](https://github.com/waylaidwanderer/node-chatgpt-api)
37 | - ChatGPT and Bing AI clients
38 | - Available as a Node.js module, REST API server, and CLI
39 | - [bing-chat](https://github.com/transitive-bullshit/bing-chat) [](https://github.com/transitive-bullshit/bing-chat)
40 | - Node.js client for Bing's new AI-powered search
41 |
42 | ### Python
43 |
44 | - [revChatGPT](https://github.com/acheong08/ChatGPT) [](https://github.com/acheong08/ChatGPT)
45 | - Most widely used ChatGPT API wrapper for Python
46 | - Supports both the official and unofficial APIs
47 | - [PyChatGPT](https://github.com/rawandahmad698/PyChatGPT) [](https://github.com/rawandahmad698/PyChatGPT)
48 | - ️ Python client for the unofficial ChatGPT API with auto token regeneration, conversation tracking, proxy support and more ⚡
49 | - [EdgeGPT](https://github.com/acheong08/EdgeGPT) [](https://github.com/acheong08/EdgeGPT)
50 | - Reverse engineered API for Microsoft's Bing Chat
51 |
52 | ### Bots
53 |
54 | - [chatgpt-twitter-bot](https://github.com/transitive-bullshit/chatgpt-twitter-bot) [](https://github.com/transitive-bullshit/chatgpt-twitter-bot)
55 | - Twitter bot powered by OpenAI's ChatGPT
56 | - [Over 90k followers](https://twitter.com/ChatGPTBot)!
57 |
58 | ### Extensions
59 |
60 | - [chatgpt-google-extension](https://github.com/wong2/chatgpt-google-extension) [](https://github.com/wong2/chatgpt-google-extension)
61 | - Browser extension which shows ChatGPT results alongside Google search results
62 |
63 | ### Applications
64 |
65 | - [Ben's Bites AI Search](https://github.com/transitive-bullshit/bens-bites-ai-search) [](https://github.com/transitive-bullshit/bens-bites-ai-search)
66 | - AI search for all the best resources in AI – powered by Ben's Bites 💯
67 | - [PandoraAI](https://github.com/waylaidwanderer/PandoraAI) [](https://github.com/waylaidwanderer/PandoraAI)
68 | - Web chat client powered by [node-chatgpt-api](https://github.com/waylaidwanderer/node-chatgpt-api), allowing users to easily chat with multiple AI systems while also offering support for custom presets
69 | - With its seamless and convenient design, PandoraAI provides an engaging conversational AI experience
70 | - [yt-semantic-search](https://github.com/transitive-bullshit/yt-semantic-search) [](https://github.com/transitive-bullshit/yt-semantic-search)
71 | - OpenAI-powered semantic search for any YouTube playlist
72 | - [Demo featuring the All-In Podcast](https://all-in-on-ai.vercel.app/) 💪
73 | - [chathub](https://github.com/chathub-dev/chathub) [](https://github.com/chathub-dev/chathub)
74 | - All-in-one chatbot client
75 |
76 | ## License
77 |
78 | NOTE: this community is not affiliated with OpenAI in any way.
79 |
80 | This website is [open source](${config.githubRepoUrl}). MIT © [${config.author}](${config.twitterUrl})
81 | `
82 |
83 | export default function AboutPage({
84 | content
85 | }: InferGetStaticPropsType) {
86 | return (
87 |
88 |
89 |
90 |
{config.title}
91 |
92 |
93 |
94 |
95 |
96 | )
97 | }
98 |
99 | export const getStaticProps = async () => {
100 | const content = await markdownToHtml(markdownContent)
101 |
102 | return {
103 | props: {
104 | content
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/pages/about/styles.module.css:
--------------------------------------------------------------------------------
1 | .aboutPage {
2 | flex: 1;
3 | width: 100%;
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | padding-top: 72px;
9 | gap: var(--gap-2);
10 | }
11 |
12 | .meta {
13 | width: 100%;
14 | display: flex;
15 | flex-direction: column;
16 | align-items: center;
17 | margin: 1em auto;
18 | }
19 |
20 | .title {
21 | font-size: 2.5rem;
22 | }
23 |
--------------------------------------------------------------------------------
/pages/index.module.css:
--------------------------------------------------------------------------------
1 | .homePage {
2 | flex: 1;
3 | width: 100%;
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | padding-top: var(--gap-h);
9 | gap: var(--gap-2);
10 | }
11 |
12 | .body {
13 | position: relative;
14 | flex: 1;
15 | width: 100%;
16 | max-width: 800px;
17 | margin: 0 auto;
18 | display: flex;
19 | flex-direction: column;
20 | justify-content: center;
21 | align-items: center;
22 | gap: var(--gap);
23 | }
24 |
25 | .section {
26 | position: relative;
27 | width: 100%;
28 | min-height: 100vh;
29 | display: flex;
30 | flex-direction: column;
31 | }
32 |
33 | .hero {
34 | justify-content: center;
35 | align-items: center;
36 | user-select: none;
37 | gap: 2em;
38 | }
39 |
40 | .title {
41 | font-family: 'Source Code Pro', 'Inter', monospace, sans-serif;
42 | text-transform: uppercase;
43 | color: #fff;
44 | text-align: center;
45 | font-size: calc(max(32px, min(128px, 18vw)));
46 | line-height: 1;
47 |
48 | /* text-shadow: 0 0 8px rgba(255, 255, 255, 1); */
49 | text-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
50 | }
51 |
52 | .desc {
53 | font-style: italic;
54 | text-align: center;
55 | line-height: 1.6;
56 | }
57 |
58 | .discordInfo {
59 | text-align: center;
60 | line-height: 1.6;
61 | }
62 |
63 | .reverse {
64 | display: inline-block;
65 | transform: translateX(0.05em) scaleX(-1);
66 | }
67 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import cs from 'clsx'
3 | // import humanizeNumber from 'human-number'
4 | import { Source_Code_Pro } from 'next/font/google'
5 |
6 | import * as config from '@/lib/config'
7 | // import { HeroButton } from '@/components/HeroButton/HeroButton'
8 | import { Button } from '@/components/Button/Button'
9 | import { Layout } from '@/components/Layout/Layout'
10 | import { MetaballVisualization } from '@/components/MetaballVisualization/MetaballVisualization'
11 | import { PageHead } from '@/components/PageHead/PageHead'
12 | import WebGLSupportChecker from '@/components/WebGLSupportChecker/WebGLSupportChecker'
13 | import { Metaballs } from '@/store/metaballs'
14 |
15 | import styles from './index.module.css'
16 |
17 | const sourceCodePro = Source_Code_Pro({ subsets: ['latin'] })
18 |
19 | export default function HomePage({
20 | numMembers,
21 | numMembersOnline
22 | }: {
23 | numMembers: string
24 | numMembersOnline: string
25 | }) {
26 | const [hasMounted, setHasMounted] = React.useState(false)
27 | React.useEffect(() => {
28 | setHasMounted(true)
29 | }, [])
30 |
31 | return (
32 |
33 |
34 |
35 |
36 | {hasMounted && (
37 |
40 | WebGL is not supported in your browser. Visualization is
41 | disabled.
42 |
43 | }
44 | >
45 |
46 |
47 | )}
48 |
49 |
50 |
51 |
52 |
53 | CHATGPT HAC
54 | K
55 | ERS
56 |
57 |
58 |
59 | Join thousands of developers, researchers, and AI enthusiasts
60 | who are building at the cutting edge of AI
61 |
62 |
63 |
64 |
Discord Members: {numMembers}
65 |
66 |
Currently Online: {numMembersOnline}
67 |
68 |
69 |
76 |
77 |
78 |
79 |
80 |
81 | )
82 | }
83 |
84 | export async function getStaticProps() {
85 | let numMembers = 7300
86 | let numMembersOnline = 100
87 |
88 | try {
89 | const discordInviteCode = config.discordUrl.split('/').pop()
90 | const res = await fetch(
91 | `https://discord.com/api/v9/invites/${discordInviteCode}?with_counts=true&with_expiration=true`
92 | )
93 |
94 | const response = await res.json()
95 | numMembers = parseInt(response.approximate_member_count) || 7300
96 | numMembersOnline = parseInt(response.approximate_presence_count) || 100
97 | } catch (err) {
98 | console.error('error fetching discord info', err)
99 | }
100 |
101 | return {
102 | props: {
103 | numMembers,
104 | numMembersOnline
105 | },
106 | // update counts lazily at most every 10 minutes (in seconds)
107 | revalidate: 10 * 60
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | 'postcss-import': {},
4 | tailwindcss: {},
5 | autoprefixer: {}
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/transitive-bullshit/chatgpt-hackers/8714eb7a73f2d1bb06d51849015e031302b88709/public/favicon.ico
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/transitive-bullshit/chatgpt-hackers/8714eb7a73f2d1bb06d51849015e031302b88709/public/icon.png
--------------------------------------------------------------------------------
/public/icon.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/logo-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/transitive-bullshit/chatgpt-hackers/8714eb7a73f2d1bb06d51849015e031302b88709/public/logo-dark.png
--------------------------------------------------------------------------------
/public/logo-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/transitive-bullshit/chatgpt-hackers/8714eb7a73f2d1bb06d51849015e031302b88709/public/logo-light.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 | Disallow: /api/*
4 |
--------------------------------------------------------------------------------
/public/social.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/transitive-bullshit/chatgpt-hackers/8714eb7a73f2d1bb06d51849015e031302b88709/public/social.jpg
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # ChatGPT Hackers Community
2 |
3 | > Website for the [ChatGPT Hackers community](https://www.chatgpthackers.dev).
4 |
5 | [](https://github.com/transitive-bullshit/chatgpt-hackers/actions/workflows/test.yml) [](https://github.com/transitive-bullshit/chatgpt-hackers/blob/main/license) [](https://prettier.io)
6 |
7 | - [Intro](#intro)
8 | - [License](#license)
9 |
10 | ## Intro
11 |
12 | This is the source of the [ChatGPT Hackers community website](https://www.chatgpthackers.dev).
13 |
14 | Join thousands of other developers, researchers, and AI enthusiasts who are building at the cutting edge of AI!
15 |
16 | ## License
17 |
18 | MIT © [Travis Fischer](https://transitivebullsh.it)
19 |
20 | NOTE: this community is not affiliated with OpenAI in any way.
21 |
22 | If you found this project interesting, please consider [sponsoring me](https://github.com/sponsors/transitive-bullshit) or following me on twitter
23 |
--------------------------------------------------------------------------------
/store/metaballs.ts:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { createContainer } from 'unstated-next'
3 |
4 | import type { MetaballViz } from '@/components/MetaballVisualization/MetaballViz'
5 |
6 | function useMetaballs() {
7 | const metaballVizRef = React.useRef(null)
8 |
9 | return {
10 | metaballVizRef
11 | }
12 | }
13 |
14 | export const Metaballs = createContainer(useMetaballs)
15 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | /* For rgb(255 115 179 / ) */
8 | --color-fg-0: 36 41 47;
9 | --color-fg-1: 51 65 85;
10 | --color-fg-2: 71 85 105;
11 | --color-fg-3: 100 116 139;
12 | --color-fg-4: 148 163 184;
13 |
14 | --color-bg-0: 255 255 255;
15 | --color-bg-1: 241 245 249;
16 | --color-bg-2: 226 232 240;
17 | --color-bg-3: 203 213 225;
18 | --color-bg-4: 148 163 184;
19 | }
20 |
21 | .dark {
22 | --color-fg-0: 255 255 255;
23 | /* --color-fg-1: 241 245 249; */
24 | --color-fg-1: 226 232 240;
25 | --color-fg-2: 203 213 225;
26 | --color-fg-3: 148 163 184;
27 | --color-fg-4: 100 116 139;
28 |
29 | --color-bg-0: 18 18 18;
30 | --color-bg-1: 51 65 85;
31 | --color-bg-2: 71 85 105;
32 | --color-bg-3: 100 116 139;
33 | --color-bg-4: 148 163 184;
34 | }
35 |
36 | .link {
37 | display: inline-block;
38 | text-decoration: none;
39 | line-height: 1.3;
40 | position: relative;
41 | transition: unset;
42 | opacity: 1;
43 | border-color: var(--fg-color-2);
44 | border-bottom-width: 0.135rem;
45 | background: transparent;
46 | background-origin: border-box;
47 | background-repeat: no-repeat;
48 | background-position: 50% 100%;
49 | background-size: 0 0.135rem;
50 | }
51 |
52 | .link:focus,
53 | .link:hover {
54 | text-decoration: none;
55 | border-bottom-color: transparent;
56 |
57 | background-image: linear-gradient(90.68deg, #b439df 0.26%, #e5337e 102.37%);
58 | background-repeat: no-repeat;
59 | background-position: 0 100%;
60 | background-size: 100% 0.135rem;
61 |
62 | transition-property: background-position, background-size;
63 | transition-duration: 300ms;
64 | }
65 | }
66 |
67 | * {
68 | box-sizing: border-box;
69 | }
70 |
71 | svg {
72 | box-sizing: border-box;
73 | }
74 |
75 | a {
76 | color: inherit;
77 | text-decoration: none;
78 | }
79 |
80 | html,
81 | body {
82 | padding: 0;
83 | margin: 0;
84 | font-family:
85 | -apple-system,
86 | BlinkMacSystemFont,
87 | Segoe UI,
88 | Roboto,
89 | Oxygen,
90 | Ubuntu,
91 | Cantarell,
92 | Fira Sans,
93 | Droid Sans,
94 | Helvetica Neue,
95 | sans-serif;
96 | }
97 |
98 | h1,
99 | h2,
100 | h3,
101 | h4,
102 | h5,
103 | h6 {
104 | margin: 0;
105 | padding: 0;
106 | }
107 |
108 | :root {
109 | --bg-color: #fff;
110 | --bg-color-0: rgba(135, 131, 120, 0.15);
111 | --bg-color-1: rgb(247, 246, 243);
112 | --bg-color-2: rgba(135, 131, 120, 0.15);
113 |
114 | --fg-color: #24292f;
115 | --fg-color-1: #292d32;
116 | --fg-color-2: #31363b;
117 |
118 | --max-width: 1200px;
119 | --max-body-width: 1024px;
120 |
121 | --gap: calc(max(8px, min(24px, 1.5vw)));
122 | --gap-w: calc(max(12px, min(48px, 1.5vw)));
123 | --gap-w-1: calc(max(2px, min(24px, 0.7vw)));
124 | --gap-h: calc(max(12px, min(48px, 4vh)));
125 | --gap-h-1: calc(max(8px, min(24px, 1.5vh)));
126 | --gap-2: calc(max(12px, min(48px, 4vmin)));
127 | }
128 |
129 | .dark {
130 | --bg-color: #121212;
131 | --bg-color-0: rgb(71, 76, 80);
132 | --bg-color-1: rgb(63, 68, 71);
133 | --bg-color-2: rgba(135, 131, 120, 0.15);
134 |
135 | --fg-color: #fff;
136 | --fg-color-1: rgba(255, 255, 255, 0.85);
137 | --fg-color-2: rgba(255, 255, 255, 0.7);
138 | }
139 |
140 | body {
141 | background: var(--bg-color);
142 | color: var(--fg-color);
143 | line-height: 1.3;
144 | }
145 |
146 | .match {
147 | border-radius: 0.35em;
148 | padding: 0.1em 0.25em;
149 | margin: -0.1em -0.25em;
150 | box-decoration-break: clone;
151 | background: #fdf59d;
152 | background: #bbebff;
153 | font-weight: normal;
154 | color: #000;
155 | }
156 |
157 | [data-radix-popper-content-wrapper] {
158 | z-index: 500 !important;
159 | }
160 |
161 | [data-nextjs-scroll-focus-boundary] {
162 | display: contents;
163 | }
164 |
--------------------------------------------------------------------------------
/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | const { fontFamily } = require('tailwindcss/defaultTheme')
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | darkMode: 'class',
6 | content: [
7 | './pages/**/*.{js,ts,jsx,tsx}',
8 | './components/**/*.{js,ts,jsx,tsx}'
9 | ],
10 | theme: {
11 | extend: {
12 | fontFamily: {
13 | sans: ['system-ui', ...fontFamily.sans]
14 | },
15 | colors: {
16 | fg: {
17 | 0: 'rgb(var(--color-fg-0) / )',
18 | 1: 'rgb(var(--color-fg-1) / )',
19 | 2: 'rgb(var(--color-fg-2) / )',
20 | 3: 'rgb(var(--color-fg-3) / )',
21 | 4: 'rgb(var(--color-fg-4) / )'
22 | },
23 | bg: {
24 | 0: 'rgb(var(--color-bg-0) / )',
25 | 1: 'rgb(var(--color-bg-1) / )',
26 | 2: 'rgb(var(--color-bg-2) / )',
27 | 3: 'rgb(var(--color-bg-3) / )',
28 | 4: 'rgb(var(--color-bg-4) / )'
29 | }
30 | }
31 | }
32 | },
33 | plugins: [require('@tailwindcss/typography')]
34 | }
35 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "strictNullChecks": false,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "incremental": true,
18 | "paths": {
19 | "@/*": ["./*"]
20 | }
21 | },
22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
23 | "exclude": ["node_modules"]
24 | }
25 |
--------------------------------------------------------------------------------