kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: move webapp to next.js
rodzic
5ab2700464
commit
6d4c551f71
|
@ -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'
|
||||
})
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
"dev": {
|
||||
"vars": {
|
||||
"ENVIRONMENT": "development",
|
||||
"AGENTIC_API_BASE_URL": "http://localhost:3000"
|
||||
"AGENTIC_API_BASE_URL": "http://localhost:3001"
|
||||
}
|
||||
},
|
||||
"prod": {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
16
|
|
@ -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
|
|
@ -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 |
|
@ -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 |
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -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
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -251,4 +251,5 @@ export {
|
|||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger}
|
||||
DropdownMenuTrigger
|
||||
}
|
||||
|
|
|
@ -56,4 +56,4 @@ function TooltipContent({
|
|||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipContent, TooltipProvider,TooltipTrigger }
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
import '@fisch0920/config/ts-reset'
|
||||
|
|
|
@ -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>()
|
|
@ -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>
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import type { Config } from 'tailwindcss'
|
||||
|
||||
export default {
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}']
|
||||
} satisfies Config
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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']
|
||||
}
|
||||
})
|
|
@ -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"
|
||||
|
|
4664
pnpm-lock.yaml
4664
pnpm-lock.yaml
Plik diff jest za duży
Load Diff
Ładowanie…
Reference in New Issue