feat: move webapp to next.js

pull/715/head
Travis Fischer 2025-06-14 10:47:40 +07:00
rodzic 5ab2700464
commit 6d4c551f71
40 zmienionych plików z 1266 dodań i 4646 usunięć

Wyświetl plik

@ -1,6 +1,6 @@
import { createClient as createAuthClient } from '@agentic/openauth/client'
export const authClient = createAuthClient({
issuer: 'http://localhost:3000',
issuer: 'http://localhost:3001',
clientID: 'agentic-internal-api-client'
})

Wyświetl plik

@ -12,7 +12,7 @@ export const envSchema = baseEnvSchema
.extend({
DATABASE_URL: z.string().url(),
PORT: z.number().default(3000),
PORT: z.number().default(3001),
STRIPE_SECRET_KEY: z.string().nonempty(),
STRIPE_WEBHOOK_SECRET: z.string().nonempty(),

Wyświetl plik

@ -60,7 +60,7 @@
"dev": {
"vars": {
"ENVIRONMENT": "development",
"AGENTIC_API_BASE_URL": "http://localhost:3000"
"AGENTIC_API_BASE_URL": "http://localhost:3001"
}
},
"prod": {

Wyświetl plik

@ -1,12 +1,11 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"rsc": true,
"tsx": true,
"iconLibrary": "lucide",
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/styles/global.css",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
@ -17,5 +16,6 @@
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
},
"iconLibrary": "lucide"
}

Wyświetl plik

@ -1 +0,0 @@
16

Wyświetl plik

@ -0,0 +1,7 @@
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
// TODO: handle remote profile pictures or upload them properly on backend
}
export default nextConfig

Wyświetl plik

@ -12,9 +12,10 @@
},
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"clean": "del dist",
"dev": "next dev",
"build": "next build",
"start": "next start",
"clean": "del .next",
"test": "run-s test:*",
"test:typecheck": "tsc --noEmit"
},
@ -27,30 +28,30 @@
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-router": "^1.121.2",
"@tanstack/react-router-devtools": "^1.121.8",
"@tanstack/react-start": "^1.121.10",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.515.0",
"motion": "^12.18.1",
"next": "^15.3.3",
"next-themes": "^0.4.6",
"posthog-js": "^1.252.0",
"react": "catalog:",
"react-dom": "catalog:",
"react-lottie-player": "^2.1.0",
"react-use": "^17.6.0",
"sonner": "^2.0.5",
"stripe": "catalog:",
"tailwind-merge": "^3.3.1",
"type-fest": "catalog:",
"vite": "^6.3.5"
"type-fest": "catalog:"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.10",
"@tailwindcss/typography": "^0.5.16",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.5",
"tailwindcss": "^4.1.10",
"tw-animate-css": "^1.3.4",
"vite-tsconfig-paths": "catalog:"
"tw-animate-css": "^1.3.4"
}
}

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 4.1 KiB

Wyświetl plik

@ -0,0 +1,9 @@
<svg width="82" height="82" viewBox="0 0 82 82" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="41" cy="41" r="41" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.4578 61.5308C19.3253 61.572 16.2142 61.5305 13.1245 61.4063C22.8506 46.0201 32.578 30.6306 42.3067 15.2374C43.7021 13.8021 45.2577 13.6154 46.9734 14.6774C49.1069 16.4727 49.7499 18.6919 48.9023 21.3352C47.3988 23.8863 45.864 26.4167 44.2978 28.9263C37.7437 39.2967 31.1897 49.6671 24.6356 60.0374C24.161 60.9068 23.4351 61.4046 22.4578 61.5308Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M47.3466 30.9175C47.6254 31.0504 47.8536 31.2577 48.031 31.5397C54.9747 41.5901 61.9643 51.6079 68.9999 61.593C65.9303 61.676 62.8607 61.676 59.791 61.593C58.9198 61.396 58.1939 60.9605 57.6133 60.2864C53.2469 53.918 48.8499 47.5714 44.4222 41.2464C43.9577 40.5736 43.543 39.8684 43.1777 39.1308C43.025 38.1776 43.2325 37.3065 43.7999 36.5175C45.0134 34.6631 46.1956 32.7965 47.3466 30.9175Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M54.5645 61.4063C51.0185 61.5305 47.451 61.5719 43.8623 61.5307C42.9149 60.1055 41.9401 58.6952 40.9378 57.2996C40.0136 58.6925 39.1217 60.1029 38.2623 61.5307C34.7564 61.5719 31.272 61.5305 27.809 61.4063C31.0789 56.2481 34.3766 51.1044 37.7023 45.9752C39.0503 44.5629 40.6474 44.2103 42.4934 44.9174C42.9837 45.1521 43.4193 45.4632 43.8001 45.8507C47.4157 51.021 51.0038 56.2063 54.5645 61.4063Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.1244 61.4063C16.2142 61.5305 19.3253 61.5719 22.4578 61.5308C19.3255 61.6961 16.173 61.6961 13 61.5308C13.0154 61.4552 13.0569 61.4138 13.1244 61.4063Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.8089 61.4063C31.2719 61.5305 34.7564 61.5719 38.2622 61.5308C34.7566 61.6961 31.2307 61.6961 27.6844 61.5308C27.6999 61.4552 27.7413 61.4138 27.8089 61.4063Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M54.5644 61.4063C54.632 61.4138 54.6734 61.4552 54.6888 61.5308C51.0596 61.6963 47.4507 61.6963 43.8622 61.5308C47.4509 61.5719 51.0184 61.5305 54.5644 61.4063Z" fill="white"/>
</svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.1 KiB

Wyświetl plik

@ -1,47 +1,13 @@
@import 'tailwindcss' source('../');
@import 'tw-animate-css';
@import 'tailwindcss';
@import 'tailwindcss/preflight';
@import 'tailwindcss/utilities';
@plugin '@tailwindcss/typography';
@custom-variant dark (&:is(.dark *));
/* @custom-variant dark (&:where(.dark, .dark *)); */
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
@theme {
--font-heading: var(--font-josefin-sans);
--font-body: var(--font-inter);
}
:root {
@ -113,6 +79,44 @@
--sidebar-ring: oklch(0.556 0 0);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
@ -121,3 +125,105 @@
@apply bg-background text-foreground;
}
}
body {
font-family: var(--font-body), Arial, Helvetica, sans-serif;
}
* {
box-sizing: border-box;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
a {
color: inherit;
text-decoration: none;
}
a:hover {
text-decoration: none;
}
a.link,
a .link {
position: relative;
transition: unset;
opacity: 1;
padding-bottom: 0.1rem;
border-bottom-width: 0.1rem;
border-bottom-color: transparent;
background: transparent;
background-origin: border-box;
background-repeat: no-repeat;
background-position: 50% 100%;
background-size: 0 0.1rem;
}
a.link:focus,
a.link:hover,
a:focus .link,
a:hover .link {
border-bottom-color: transparent;
background-image: linear-gradient(90.68deg, #b439df 0.26%, #e5337e 102.37%);
background-repeat: no-repeat;
background-position: 0 100%;
background-size: 100% 0.1rem;
transition-property: background-position, background-size;
transition-duration: 300ms;
}
h1 {
font-size: 2.5rem;
font-weight: 600;
@apply mb-8;
}
h2 {
font-size: 2rem;
font-weight: 500;
@apply mb-7;
}
h3 {
font-size: 1.5rem;
font-weight: 450;
margin-bottom: 1em;
@apply mb-6;
}
h4 {
font-size: 1.3rem;
font-weight: 400;
@apply mb-5;
}
h5 {
font-size: 1.2rem;
font-weight: 350;
@apply mb-4;
}
h6 {
font-size: 1rem;
font-weight: 350;
@apply mb-2;
}
main section {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
@apply mb-16;
}
main section:last-of-type {
margin-bottom: 0;
}

Wyświetl plik

@ -0,0 +1,78 @@
import './globals.css'
import type { Metadata } from 'next'
import cs from 'clsx'
import { Inter } from 'next/font/google'
import { Toaster } from 'sonner'
import { Bootstrap } from '@/components/bootstrap'
import { Footer } from '@/components/footer'
import { Header } from '@/components/header'
// import { PostHogProvider } from '@/components/posthog-provider'
import { ThemeProvider } from '@/components/theme-provider'
import * as config from '@/lib/config'
import styles from './styles.module.css'
const inter = Inter({
variable: '--font-inter',
subsets: ['latin']
})
// const josefinSans = Josefin_Sans({
// variable: '--font-josefin-sans',
// subsets: ['latin']
// })
export const metadata: Metadata = {
title: config.title,
description: config.description,
authors: [{ name: config.author, url: config.twitterUrl }],
metadataBase: new URL(config.prodUrl),
keywords: config.keywords,
openGraph: {
title: config.title,
description: config.description,
siteName: config.title,
locale: 'en_US',
type: 'website',
url: config.prodUrl
},
twitter: {
card: 'summary_large_image',
creator: `@${config.authorTwitterUsername}`,
title: config.title,
description: config.description
}
}
export default function RootLayout({
children
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang='en' suppressHydrationWarning>
<body className={`${inter.variable} antialiased`}>
<ThemeProvider
attribute='class'
defaultTheme='dark'
disableTransitionOnChange
>
<div className={styles.root}>
<Header />
<main className={cs(styles.main, 'pt-8 pb-16 px-4 md:px-0')}>
{children}
</main>
<Toaster richColors />
<Footer />
</div>
</ThemeProvider>
<Bootstrap />
</body>
</html>
)
}

Wyświetl plik

@ -0,0 +1,25 @@
import { HeroButton } from '@/components/hero-button'
export default function Page() {
return (
<>
<section>
<h1 className='my-0! text-center text-balance leading-snug md:leading-none'>
Agentic MCP Gateway
</h1>
<h5 className='my-8! text-center text-balance'>
An API gateway built exclusively for AI agents.
</h5>
<HeroButton>Get Started</HeroButton>
</section>
<section className='flex-1'>
<h2 className='text-center text-balance'>How it works</h2>
<div>TODO</div>
</section>
</>
)
}

Wyświetl plik

@ -0,0 +1,16 @@
.root {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
width: 100%;
}
.main {
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
max-width: 1200px;
}

Wyświetl plik

@ -0,0 +1,84 @@
'use client'
import cs from 'clsx'
import Link, { type LinkProps } from 'next/link'
import { usePathname } from 'next/navigation'
import * as React from 'react'
type ActiveLinkProps = LinkProps & {
children?: React.ReactNode
className?: string
activeClassName?: string
style?: React.CSSProperties
// optional comparison function to normalize URLs before comparing
compare?: (a?: any, b?: any) => boolean
}
/**
* Link that will be disabled if the target `href` is the same as the current
* route's pathname.
*/
export const ActiveLink = React.forwardRef(function ActiveLink(
{
children,
href,
style,
className,
activeClassName,
onClick,
prefetch,
compare = (a, b) => a === b,
...props
}: ActiveLinkProps,
ref
) {
const pathname = usePathname()
const [disabled, setDisabled] = React.useState(false)
React.useEffect(() => {
const linkPathname = new URL(href as string, location.href).pathname
setDisabled(compare(linkPathname, pathname))
}, [pathname, href, compare])
const styleOverride = React.useMemo<React.CSSProperties>(
() =>
disabled
? {
...style,
pointerEvents: 'none'
}
: (style ?? {}),
[disabled, style]
)
const onClickOverride = React.useCallback(
(event: any): void => {
if (disabled) {
event.preventDefault()
return
}
if (onClick) {
onClick(event)
return
}
},
[disabled, onClick]
)
return (
<Link
{...props}
className={cs(className, disabled && activeClassName)}
href={href}
prefetch={disabled ? false : prefetch}
style={styleOverride}
onClick={onClickOverride}
ref={ref as any}
>
{children}
</Link>
)
})

Wyświetl plik

@ -0,0 +1,15 @@
'use client'
import { useFirstMountState } from 'react-use'
import { bootstrap } from '@/lib/bootstrap'
export function Bootstrap() {
const isFirstMount = useFirstMountState()
if (isFirstMount) {
bootstrap()
}
return null
}

Wyświetl plik

@ -0,0 +1,42 @@
'use client'
import { Moon, Sun } from 'lucide-react'
import { useTheme } from 'next-themes'
import * as React from 'react'
import { Button } from '@/components/ui/button'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '@/components/ui/tooltip'
export function DarkModeToggle() {
const { setTheme, resolvedTheme } = useTheme()
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='outline'
size='icon'
className='cursor-pointer'
onClick={() =>
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')
}
>
<Sun className='h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0' />
<Moon className='absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100' />
<span className='sr-only'>Toggle theme</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<span>Toggle dark mode</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}

Wyświetl plik

@ -1,55 +0,0 @@
import {
ErrorComponent,
type ErrorComponentProps,
Link,
rootRouteId,
useMatch,
useRouter
} from '@tanstack/react-router'
export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
const router = useRouter()
const isRoot = useMatch({
strict: false,
select: (state) => state.id === rootRouteId
})
console.error('DefaultCatchBoundary Error:', error)
return (
<div className='min-w-0 flex-1 p-4 flex flex-col items-center justify-center gap-6'>
<ErrorComponent error={error} />
<div className='flex gap-2 items-center flex-wrap'>
<button
onClick={() => {
void router.invalidate()
}}
className='px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded text-white uppercase font-extrabold'
>
Try Again
</button>
{isRoot ? (
<Link
to='/'
className='px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded text-white uppercase font-extrabold'
>
Home
</Link>
) : (
<Link
to='/'
className='px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded text-white uppercase font-extrabold'
onClick={(e) => {
e.preventDefault()
globalThis.history.back()
}}
>
Go Back
</Link>
)}
</div>
</div>
)
}

Wyświetl plik

@ -0,0 +1,81 @@
import Link from 'next/link'
import { ActiveLink } from '@/components/active-link'
import { GitHub } from '@/icons/github'
import { Twitter } from '@/icons/twitter'
import { copyright, githubUrl, twitterUrl } from '@/lib/config'
export function Footer() {
return (
<footer className='w-full py-12 border-t flex flex-col items-center'>
<div className='container px-4 md:px-6 max-w-1200px'>
<div className='flex flex-col md:grid md:grid-cols-4 gap-8'>
<div className='flex flex-col md:items-center'>
<div className='space-y-4'>
<h3 className='text-lg font-semibold'>Site</h3>
<nav className='flex flex-col space-y-2'>
<span>
<ActiveLink href='/' className='link'>
Home
</ActiveLink>
</span>
<span>
<ActiveLink href='/about' className='link'>
About
</ActiveLink>
</span>
<span>
<ActiveLink href='/legal' className='link'>
Legal
</ActiveLink>
</span>
</nav>
</div>
</div>
<div className='flex flex-col order-last md:order-none col-span-2'>
<div className='space-y-4 flex flex-col w-full'>
<h3 className='text-lg font-semibold'>TODO</h3>
<div className='grid grid-cols-[repeat(auto-fill,_minmax(10em,_1fr))] gap-y-4 gap-x-8 w-full flex-auto'>
<div className='link'>TODO</div>
</div>
</div>
</div>
<div className='flex flex-col md:items-center'>
<div className='space-y-4'>
<h3 className='text-lg font-semibold'>Social</h3>
<nav className='flex flex-col space-y-2'>
<Link
href={twitterUrl}
className='flex items-center space-x-2'
target='_blank'
rel='noopener noreferrer'
>
<Twitter className='h-4 w-4' />
<span>Twitter</span>
</Link>
<Link
href={githubUrl}
className='flex items-center space-x-2'
target='_blank'
rel='noopener noreferrer'
>
<GitHub className='h-4 w-4' />
<span>GitHub</span>
</Link>
</nav>
</div>
</div>
</div>
<div className='mt-8 pt-8 border-t border-border text-center text-sm text-muted-foreground'>
<span>{copyright}</span>
</div>
</div>
</footer>
)
}

Wyświetl plik

@ -0,0 +1,34 @@
import cs from 'clsx'
import { ActiveLink } from '@/components/active-link'
import { DarkModeToggle } from '@/components/dark-mode-toggle'
import styles from './styles.module.css'
export function Header() {
return (
<header className={styles.header}>
<div className={styles.headerContent}>
<ActiveLink className={styles.logo} href='/'>
Lumon
</ActiveLink>
<div className='md:hidden'>
<ActiveLink href='/about' className='link'>
About
</ActiveLink>
</div>
<div className={cs(styles.rhs)}>
<div className='hidden md:block'>
<ActiveLink href='/about' className='link mr-2'>
About
</ActiveLink>
</div>
<DarkModeToggle />
</div>
</div>
</header>
)
}

Wyświetl plik

@ -0,0 +1,59 @@
.header {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 12px 0;
line-height: 1;
}
.headerContent {
pointer-events: auto;
align-self: center;
width: 100%;
max-width: 1200px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 1em;
min-height: 32px;
}
.rhs {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
height: 100%;
gap: 0.5em;
}
.logo {
font-family: 'Josefin Sans';
font-weight: bold;
text-transform: uppercase;
font-size: 2em;
letter-spacing: 0.02em;
user-select: none;
}
.navHeader {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: var(--gap-w-1);
max-width: var(--max-width);
height: 100%;
margin: 0 auto;
}
.logo a:link,
.logo a:visited,
.logo a:hover,
.logo a:active {
text-decoration: none;
color: inherit;
}

Wyświetl plik

@ -1,27 +0,0 @@
import { Link } from '@tanstack/react-router'
export function NotFound({ children }: { children?: any }) {
return (
<div className='space-y-2 p-2'>
<div className='text-gray-600 dark:text-gray-400'>
{children || <p>The page you are looking for does not exist.</p>}
</div>
<p className='flex items-center gap-2 flex-wrap'>
<button
onClick={() => globalThis.history.back()}
className='bg-emerald-500 text-white px-2 py-1 rounded uppercase font-black text-sm'
>
Go back
</button>
<Link
to='/'
className='bg-cyan-600 text-white px-2 py-1 rounded uppercase font-black text-sm'
>
Start Over
</Link>
</p>
</div>
)
}

Wyświetl plik

@ -0,0 +1,69 @@
'use client'
import { usePathname, useSearchParams } from 'next/navigation'
import { posthog } from 'posthog-js'
import { PostHogProvider as PHProvider, usePostHog } from 'posthog-js/react'
import { Suspense, useEffect, useState } from 'react'
import { posthogHost, posthogKey } from '@/lib/config'
export function PostHogProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (posthogKey) {
posthog.init(posthogKey, {
api_host: posthogHost,
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
capture_pageview: false // Disable automatic pageview capture, as we capture manually
})
}
}, [])
if (!posthogKey) {
return children
}
return (
<PHProvider client={posthog}>
<SuspendedPostHogPageView />
{children}
</PHProvider>
)
}
function PostHogPageView() {
const pathname = usePathname()
const searchParams = useSearchParams()
const posthog = usePostHog()
const [prevPathname, setPrevPathname] = useState<string | null>(null)
// Track pageviews
useEffect(() => {
if (pathname && posthog) {
let url = globalThis.window.origin + pathname
if (searchParams.toString()) {
url = url + '?' + searchParams.toString()
}
if (prevPathname && prevPathname !== pathname) {
posthog.capture('$pageleave', { $pathname: prevPathname })
}
posthog.capture('$pageview', { $current_url: url })
setPrevPathname(pathname)
}
}, [pathname, prevPathname, searchParams, posthog])
return null
}
// Wrap PostHogPageView in Suspense to avoid the useSearchParams usage above
// from de-opting the whole app into client-side rendering
// See: https://nextjs.org/docs/messages/deopted-into-client-rendering
function SuspendedPostHogPageView() {
return (
<Suspense fallback={null}>
<PostHogPageView />
</Suspense>
)
}

Wyświetl plik

@ -1,81 +1,11 @@
import {
createContext,
type ReactNode,
useContext,
useEffect,
useState
} from 'react'
'use client'
type Theme = 'dark' | 'light' | 'system'
type ThemeProviderProps = {
children: ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: 'system',
setTheme: () => null
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
import type React from 'react'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'vite-ui-theme',
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() =>
(globalThis.localStorage?.getItem(storageKey) as Theme) || defaultTheme
)
useEffect(() => {
const root = globalThis.document.documentElement
root.classList.remove('light', 'dark')
if (theme === 'system') {
const systemTheme = globalThis.matchMedia('(prefers-color-scheme: dark)')
.matches
? 'dark'
: 'light'
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
globalThis.localStorage!.setItem(storageKey, theme)
setTheme(theme)
}
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return context
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

Wyświetl plik

@ -1,42 +0,0 @@
import { Moon, Sun } from 'lucide-react'
import { useTheme } from '@/components/theme-provider'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
export function ModeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='outline' size='icon'>
<Sun className='h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90' />
<Moon className='absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0' />
<span className='sr-only'>Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => setTheme('light')}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

Wyświetl plik

@ -251,4 +251,5 @@ export {
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger}
DropdownMenuTrigger
}

Wyświetl plik

@ -56,4 +56,4 @@ function TooltipContent({
)
}
export { Tooltip, TooltipContent, TooltipProvider,TooltipTrigger }
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }

Wyświetl plik

@ -0,0 +1,7 @@
export function GitHub({ className }: { className?: string }) {
return (
<svg viewBox='0 0 24 24' fill='currentcolor' className={className}>
<path d='M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22'></path>
</svg>
)
}

Wyświetl plik

@ -0,0 +1,13 @@
export function Twitter({ className }: { className?: string }) {
return (
<svg
role='img'
viewBox='0 0 24 24'
fill='currentcolor'
className={className}
>
<title>X</title>
<path d='M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z' />
</svg>
)
}

Wyświetl plik

@ -1,3 +1,4 @@
/* eslint-disable no-process-env */
export const isServer = globalThis.window === undefined
export const isSafari =
!isServer && /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
@ -6,7 +7,7 @@ export const title = 'Agentic'
export const description =
'Agentic is an API gateway built exclusively for AI agents.'
export const domain =
import.meta.env.VITE_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL ?? 'agentic.so'
process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL ?? 'agentic.so'
export const author = 'Travis Fischer'
export const authorTwitterUsername = 'transitive_bs'
@ -14,24 +15,43 @@ export const twitterUrl = `https://x.com/${authorTwitterUsername}`
export const copyright = `© ${new Date().getFullYear()} Agentic. All rights reserved.`
export const githubUrl = 'https://github.com/transitive-bullshit/agentic'
export const keywords = [
'agentic',
'MCP',
'Model Context Protocol',
'MCP gateway',
'API gateway',
'MCP marketplace',
'MCP API gateway',
'MCP monetization',
'production MCPs',
'ai',
'AI tools',
'AI agents',
'LLM tools',
'MCP servers',
'MCP server provider',
'MCP server deployment',
'OpenAPI to MCP',
'OpenAPI to MCP server'
]
export const env =
import.meta.env.VITE_PUBLIC_VERCEL_ENV ??
import.meta.env.NODE_ENV ??
'development'
process.env.NEXT_PUBLIC_VERCEL_ENV ?? process.env.NODE_ENV ?? 'development'
export const isVercel = !!(
import.meta.env.VITE_PUBLIC_VERCEL_ENV || import.meta.env.VERCEL
process.env.NEXT_PUBLIC_VERCEL_ENV || process.env.VERCEL
)
export const isDev = env === 'development' && !isVercel
export const isProd = env === 'production'
export const isTest = env === 'test'
export const port = import.meta.env.PORT || '3000'
export const port = process.env.PORT || '3000'
export const prodUrl = `https://${domain}`
export const url = isDev ? `http://localhost:${port}` : prodUrl
export const vercelUrl =
import.meta.env.VERCEL_URL ?? import.meta.env.VITE_PUBLIC_VERCEL_URL
process.env.VERCEL_URL ?? process.env.NEXT_PUBLIC_VERCEL_URL
export const apiBaseUrl = isDev || !vercelUrl ? url : `https://${vercelUrl}`
export const posthogKey = import.meta.env.VITE_PUBLIC_POSTHOG_KEY!
export const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY!
export const posthogHost =
import.meta.env.VITE_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com'
process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com'

Wyświetl plik

@ -1,3 +1 @@
/// <reference types="vite/client" />
import '@fisch0920/config/ts-reset'

Wyświetl plik

@ -1,59 +0,0 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/'
fileRoutesByTo: FileRoutesByTo
to: '/'
id: '__root__' | '/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

Wyświetl plik

@ -1,24 +0,0 @@
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { NotFound } from './components/not-found'
import { bootstrap } from './lib/bootstrap'
import { routeTree } from './routeTree.gen'
export function createRouter() {
bootstrap()
const router = createTanStackRouter({
routeTree,
defaultPreload: 'intent',
scrollRestoration: true,
defaultNotFoundComponent: () => <NotFound />
})
return router
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createRouter>
}
}

Wyświetl plik

@ -1,72 +0,0 @@
import type { ReactNode } from 'react'
import {
createRootRoute,
HeadContent,
Outlet,
Scripts
} from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
import { PostHogProvider } from 'posthog-js/react'
import { ThemeProvider } from '@/components/theme-provider'
import { posthogHost, posthogKey } from '@/lib/config'
import globalStyles from '@/styles/global.css?url'
export const Route = createRootRoute({
head: () => ({
meta: [
{
// eslint-disable-next-line unicorn/text-encoding-identifier-case
charSet: 'utf-8'
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1'
},
{
title: 'Agentic'
}
],
links: [
{
rel: 'stylesheet',
href: globalStyles
}
]
}),
component: RootComponent
})
function RootComponent() {
return (
<RootDocument>
<Outlet />
</RootDocument>
)
}
const posthogOptions = {
api_host: posthogHost,
defaults: '2025-05-24'
} as const
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
return (
<html lang='en'>
<head>
<HeadContent />
</head>
<body>
<PostHogProvider apiKey={posthogKey} options={posthogOptions}>
<ThemeProvider defaultTheme='dark' storageKey='agentic-ui-theme'>
{children}
</ThemeProvider>
</PostHogProvider>
<TanStackRouterDevtools position='bottom-right' />
<Scripts />
</body>
</html>
)
}

Wyświetl plik

@ -1,46 +0,0 @@
import * as fs from 'node:fs/promises'
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
const filePath = 'count.txt'
async function readCount() {
return Number.parseInt(await fs.readFile(filePath, 'utf8').catch(() => '0'))
}
const getCount = createServerFn({
method: 'GET'
}).handler(() => {
return readCount()
})
const updateCount = createServerFn({ method: 'POST' })
.validator((d: number) => d)
.handler(async ({ data }) => {
const count = await readCount()
await fs.writeFile(filePath, `${count + data}`)
})
export const Route = createFileRoute('/')({
component: Home,
loader: async () => getCount()
})
function Home() {
const router = useRouter()
const state = Route.useLoaderData()
return (
<button
type='button'
onClick={() => {
void updateCount({ data: 1 }).then(() => {
return router.invalidate()
})
}}
>
Add 1 to {state}?
</button>
)
}

Wyświetl plik

@ -1,5 +0,0 @@
import type { Config } from 'tailwindcss'
export default {
content: ['./src/**/*.{js,jsx,ts,tsx}']
} satisfies Config

Wyświetl plik

@ -1,14 +1,16 @@
{
"extends": "@fisch0920/config/tsconfig-react",
"compilerOptions": {
// https://tanstack.com/start/latest/docs/framework/react/build-from-scratch#typescript-configuration
"verbatimModuleSyntax": false,
"jsx": "react-jsx",
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src", "*.config.ts"],
"exclude": ["node_modules", "dist"]
"include": ["src", "*.config.ts", "next-env.d.ts", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

Wyświetl plik

@ -1,18 +0,0 @@
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { defineConfig } from 'vite'
import tsConfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
server: {
port: 3001
},
plugins: [
tsConfigPaths(),
tanstackStart({
target: 'vercel'
})
],
ssr: {
noExternal: ['posthog-js', 'posthog-js/react']
}
})

Wyświetl plik

@ -17,7 +17,7 @@
".": "./src/index.ts"
},
"scripts": {
"generate": "openapi-typescript http://localhost:3000/docs --output ./src/openapi.d.ts",
"generate": "openapi-typescript http://localhost:3001/docs --output ./src/openapi.d.ts",
"test": "run-s test:*",
"test:typecheck": "tsc --noEmit",
"test:unit": "vitest run"

Plik diff jest za duży Load Diff