first commit
This commit is contained in:
112
src/components/Header.tsx
Normal file
112
src/components/Header.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
|
||||
import ParaglideLocaleSwitcher from './LocaleSwitcher.tsx'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Globe, Home, Languages, Menu, Network, X } from 'lucide-react'
|
||||
|
||||
export default function Header() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="p-4 flex items-center bg-gray-800 text-white shadow-lg">
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="p-2 hover:bg-gray-700 rounded-lg transition-colors"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<Menu size={24} />
|
||||
</button>
|
||||
<h1 className="ml-4 text-xl font-semibold">
|
||||
<Link to="/">
|
||||
<img
|
||||
src="/tanstack-word-logo-white.svg"
|
||||
alt="TanStack Logo"
|
||||
className="h-10"
|
||||
/>
|
||||
</Link>
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<aside
|
||||
className={`fixed top-0 left-0 h-full w-80 bg-gray-900 text-white shadow-2xl z-50 transform transition-transform duration-300 ease-in-out flex flex-col ${
|
||||
isOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-700">
|
||||
<h2 className="text-xl font-bold">Navigation</h2>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="p-2 hover:bg-gray-800 rounded-lg transition-colors"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 overflow-y-auto">
|
||||
<Link
|
||||
to="/"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
|
||||
activeProps={{
|
||||
className:
|
||||
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
|
||||
}}
|
||||
>
|
||||
<Home size={20} />
|
||||
<span className="font-medium">Home</span>
|
||||
</Link>
|
||||
|
||||
{/* Demo Links Start */}
|
||||
|
||||
<Link
|
||||
to="/demo/tanstack-query"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
|
||||
activeProps={{
|
||||
className:
|
||||
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
|
||||
}}
|
||||
>
|
||||
<Network size={20} />
|
||||
<span className="font-medium">TanStack Query</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/demo/sentry/testing"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
|
||||
activeProps={{
|
||||
className:
|
||||
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
|
||||
}}
|
||||
>
|
||||
<Globe size={20} />
|
||||
<span className="font-medium">Sentry</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/demo/i18n"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
|
||||
activeProps={{
|
||||
className:
|
||||
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
|
||||
}}
|
||||
>
|
||||
<Languages size={20} />
|
||||
<span className="font-medium">I18n example</span>
|
||||
</Link>
|
||||
|
||||
{/* Demo Links End */}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-gray-700 bg-gray-800 flex flex-col gap-2">
|
||||
<ParaglideLocaleSwitcher />
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
)
|
||||
}
|
||||
47
src/components/LocaleSwitcher.tsx
Normal file
47
src/components/LocaleSwitcher.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
// Locale switcher refs:
|
||||
// - Paraglide docs: https://inlang.com/m/gerre34r/library-inlang-paraglideJs
|
||||
// - Router example: https://github.com/TanStack/router/tree/main/examples/react/i18n-paraglide#switching-locale
|
||||
|
||||
import { m } from "@/paraglide/messages"
|
||||
import { getLocale, locales, setLocale } from "@/paraglide/runtime"
|
||||
|
||||
export default function ParaglideLocaleSwitcher() {
|
||||
const currentLocale = getLocale()
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "0.5rem",
|
||||
alignItems: "center",
|
||||
color: "inherit",
|
||||
}}
|
||||
aria-label={m.language_label()}
|
||||
>
|
||||
<span style={{ opacity: 0.85 }}>
|
||||
{m.current_locale({ locale: currentLocale })}
|
||||
</span>
|
||||
<div style={{ display: "flex", gap: "0.25rem" }}>
|
||||
{locales.map((locale) => (
|
||||
<button
|
||||
key={locale}
|
||||
onClick={() => setLocale(locale)}
|
||||
aria-pressed={locale === currentLocale}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
padding: "0.35rem 0.75rem",
|
||||
borderRadius: "999px",
|
||||
border: "1px solid #d1d5db",
|
||||
background: locale === currentLocale ? "#0f172a" : "transparent",
|
||||
color: locale === currentLocale ? "#f8fafc" : "inherit",
|
||||
fontWeight: locale === currentLocale ? 700 : 500,
|
||||
letterSpacing: "0.01em",
|
||||
}}
|
||||
>
|
||||
{locale.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
6
src/integrations/tanstack-query/devtools.tsx
Normal file
6
src/integrations/tanstack-query/devtools.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
|
||||
|
||||
export default {
|
||||
name: 'Tanstack Query',
|
||||
render: <ReactQueryDevtoolsPanel />,
|
||||
}
|
||||
34
src/integrations/tanstack-query/root-provider.tsx
Normal file
34
src/integrations/tanstack-query/root-provider.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
let context:
|
||||
| {
|
||||
queryClient: QueryClient
|
||||
}
|
||||
| undefined
|
||||
|
||||
export function getContext() {
|
||||
if (context) {
|
||||
return context
|
||||
}
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
context = {
|
||||
queryClient,
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export default function TanStackQueryProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) {
|
||||
const { queryClient } = getContext()
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
7
src/lib/utils.ts
Normal file
7
src/lib/utils.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { ClassValue } from 'clsx'
|
||||
import { clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
131
src/routeTree.gen.ts
Normal file
131
src/routeTree.gen.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/* 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'
|
||||
import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query'
|
||||
import { Route as DemoI18nRouteImport } from './routes/demo.i18n'
|
||||
import { Route as DemoSentryTestingRouteImport } from './routes/demo/sentry.testing'
|
||||
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DemoTanstackQueryRoute = DemoTanstackQueryRouteImport.update({
|
||||
id: '/demo/tanstack-query',
|
||||
path: '/demo/tanstack-query',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DemoI18nRoute = DemoI18nRouteImport.update({
|
||||
id: '/demo/i18n',
|
||||
path: '/demo/i18n',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DemoSentryTestingRoute = DemoSentryTestingRouteImport.update({
|
||||
id: '/demo/sentry/testing',
|
||||
path: '/demo/sentry/testing',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/demo/i18n': typeof DemoI18nRoute
|
||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||
'/demo/sentry/testing': typeof DemoSentryTestingRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/demo/i18n': typeof DemoI18nRoute
|
||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||
'/demo/sentry/testing': typeof DemoSentryTestingRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/demo/i18n': typeof DemoI18nRoute
|
||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||
'/demo/sentry/testing': typeof DemoSentryTestingRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/demo/i18n'
|
||||
| '/demo/tanstack-query'
|
||||
| '/demo/sentry/testing'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/demo/i18n' | '/demo/tanstack-query' | '/demo/sentry/testing'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/demo/i18n'
|
||||
| '/demo/tanstack-query'
|
||||
| '/demo/sentry/testing'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
DemoI18nRoute: typeof DemoI18nRoute
|
||||
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
|
||||
DemoSentryTestingRoute: typeof DemoSentryTestingRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/demo/tanstack-query': {
|
||||
id: '/demo/tanstack-query'
|
||||
path: '/demo/tanstack-query'
|
||||
fullPath: '/demo/tanstack-query'
|
||||
preLoaderRoute: typeof DemoTanstackQueryRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/demo/i18n': {
|
||||
id: '/demo/i18n'
|
||||
path: '/demo/i18n'
|
||||
fullPath: '/demo/i18n'
|
||||
preLoaderRoute: typeof DemoI18nRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/demo/sentry/testing': {
|
||||
id: '/demo/sentry/testing'
|
||||
path: '/demo/sentry/testing'
|
||||
fullPath: '/demo/sentry/testing'
|
||||
preLoaderRoute: typeof DemoSentryTestingRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
DemoI18nRoute: DemoI18nRoute,
|
||||
DemoTanstackQueryRoute: DemoTanstackQueryRoute,
|
||||
DemoSentryTestingRoute: DemoSentryTestingRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
import type { getRouter } from './router.tsx'
|
||||
import type { createStart } from '@tanstack/react-start'
|
||||
declare module '@tanstack/react-start' {
|
||||
interface Register {
|
||||
ssr: true
|
||||
router: Awaited<ReturnType<typeof getRouter>>
|
||||
}
|
||||
}
|
||||
24
src/router.tsx
Normal file
24
src/router.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
|
||||
import { getContext } from './integrations/tanstack-query/root-provider'
|
||||
|
||||
export function getRouter() {
|
||||
const router = createTanStackRouter({
|
||||
routeTree,
|
||||
|
||||
context: getContext(),
|
||||
|
||||
scrollRestoration: true,
|
||||
defaultPreload: 'intent',
|
||||
defaultPreloadStaleTime: 0,
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: ReturnType<typeof getRouter>
|
||||
}
|
||||
}
|
||||
78
src/routes/__root.tsx
Normal file
78
src/routes/__root.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { TanStackDevtools } from "@tanstack/react-devtools";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
createRootRouteWithContext,
|
||||
HeadContent,
|
||||
Scripts,
|
||||
} from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
||||
import { getLocale } from "@/paraglide/runtime";
|
||||
import Header from "../components/Header";
|
||||
import TanStackQueryDevtools from "../integrations/tanstack-query/devtools";
|
||||
import TanStackQueryProvider from "../integrations/tanstack-query/root-provider";
|
||||
import appCss from "../styles.css?url";
|
||||
|
||||
interface MyRouterContext {
|
||||
queryClient: QueryClient;
|
||||
}
|
||||
|
||||
export const Route = createRootRouteWithContext<MyRouterContext>()({
|
||||
beforeLoad: async () => {
|
||||
// Other redirect strategies are possible; see
|
||||
// https://github.com/TanStack/router/tree/main/examples/react/i18n-paraglide#offline-redirect
|
||||
if (typeof document !== "undefined") {
|
||||
document.documentElement.setAttribute("lang", getLocale());
|
||||
}
|
||||
},
|
||||
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
charSet: "utf-8",
|
||||
},
|
||||
{
|
||||
name: "viewport",
|
||||
content: "width=device-width, initial-scale=1",
|
||||
},
|
||||
{
|
||||
title: "TanStack Start Starter",
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: appCss,
|
||||
},
|
||||
],
|
||||
}),
|
||||
shellComponent: RootDocument,
|
||||
});
|
||||
|
||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang={getLocale()}>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
<TanStackQueryProvider>
|
||||
<Header />
|
||||
{children}
|
||||
<TanStackDevtools
|
||||
config={{
|
||||
position: "bottom-right",
|
||||
}}
|
||||
plugins={[
|
||||
{
|
||||
name: "Tanstack Router",
|
||||
render: <TanStackRouterDevtoolsPanel />,
|
||||
},
|
||||
TanStackQueryDevtools,
|
||||
]}
|
||||
/>
|
||||
</TanStackQueryProvider>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
28
src/routes/demo.i18n.tsx
Normal file
28
src/routes/demo.i18n.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { m } from "@/paraglide/messages";
|
||||
import LocaleSwitcher from "../components/LocaleSwitcher";
|
||||
|
||||
export const Route = createFileRoute("/demo/i18n")({
|
||||
component: App,
|
||||
});
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<header className="min-h-screen flex flex-col items-center justify-center bg-[#282c34] text-white text-[calc(10px+2vmin)] gap-4">
|
||||
<p>{m.example_message({ username: "TanStack Router" })}</p>
|
||||
<a
|
||||
className="text-[#61dafb] hover:underline"
|
||||
href="https://inlang.com/m/gerre34r/library-inlang-paraglideJs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{m.learn_router()}
|
||||
</a>
|
||||
<div className="mt-3">
|
||||
<LocaleSwitcher />
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
621
src/routes/demo/sentry.testing.tsx
Normal file
621
src/routes/demo/sentry.testing.tsx
Normal file
@@ -0,0 +1,621 @@
|
||||
/**
|
||||
* FILE OVERVIEW:
|
||||
* Purpose: Interactive demo page showcasing Sentry's monitoring capabilities
|
||||
* Key Concepts: Error tracking, Performance monitoring, Session replay
|
||||
* Module Type: Route Component
|
||||
* @ai_context: Demonstrates Sentry features through interactive examples with educational context
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs/promises'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createServerFn } from '@tanstack/react-start'
|
||||
import * as Sentry from '@sentry/tanstackstart-react'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export const Route = createFileRoute('/demo/sentry/testing')({
|
||||
component: RouteComponent,
|
||||
errorComponent: ({ error }) => {
|
||||
useEffect(() => {
|
||||
Sentry.captureException(error)
|
||||
}, [error])
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[#181423]">
|
||||
<div className="text-center p-8">
|
||||
<SentryLogo />
|
||||
<h1 className="text-2xl font-bold text-white mt-4 mb-2">
|
||||
Something went wrong
|
||||
</h1>
|
||||
<p className="text-[#A49FB5]">{error.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// Sentry Logo Component
|
||||
function SentryLogo({ size = 48 }: { size?: number }) {
|
||||
return (
|
||||
<svg
|
||||
height={size}
|
||||
width={size}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 40 40"
|
||||
>
|
||||
<path
|
||||
d="M21.85 2.995a3.698 3.698 0 0 1 1.353 1.354l16.303 28.278a3.703 3.703 0 0 1-1.354 5.053 3.694 3.694 0 0 1-1.848.496h-3.828a31.149 31.149 0 0 0 0-3.09h3.815a.61.61 0 0 0 .537-.917L20.523 5.893a.61.61 0 0 0-1.057 0l-3.739 6.494a28.948 28.948 0 0 1 9.63 10.453 28.988 28.988 0 0 1 3.499 13.78v1.542h-9.852v-1.544a19.106 19.106 0 0 0-2.182-8.85 19.08 19.08 0 0 0-6.032-6.829l-1.85 3.208a15.377 15.377 0 0 1 6.382 12.484v1.542H3.696A3.694 3.694 0 0 1 0 34.473c0-.648.17-1.286.494-1.849l2.33-4.074a8.562 8.562 0 0 1 2.689 1.536L3.158 34.17a.611.611 0 0 0 .538.917h8.448a12.481 12.481 0 0 0-6.037-9.09l-1.344-.772 4.908-8.545 1.344.77a22.16 22.16 0 0 1 7.705 7.444 22.193 22.193 0 0 1 3.316 10.193h3.699a25.892 25.892 0 0 0-3.811-12.033 25.856 25.856 0 0 0-9.046-8.796l-1.344-.772 5.269-9.136a3.698 3.698 0 0 1 3.2-1.849c.648 0 1.285.17 1.847.495Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Server function that will error
|
||||
const badServerFunc = createServerFn({
|
||||
method: 'GET',
|
||||
}).handler(async () => {
|
||||
return await Sentry.startSpan(
|
||||
{
|
||||
name: 'Reading non-existent file',
|
||||
op: 'file.read',
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
await fs.readFile('./doesnt-exist', 'utf-8')
|
||||
return true
|
||||
} catch (error) {
|
||||
Sentry.captureException(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
// Server function that will succeed but be traced
|
||||
const goodServerFunc = createServerFn({
|
||||
method: 'GET',
|
||||
}).handler(async () => {
|
||||
return await Sentry.startSpan(
|
||||
{
|
||||
name: 'Successful server operation',
|
||||
op: 'demo.success',
|
||||
},
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
return { success: true }
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
// 3D Button Component inspired by Sentry wizard
|
||||
function SentryButton({
|
||||
children,
|
||||
onClick,
|
||||
variant = 'primary',
|
||||
disabled = false,
|
||||
loading = false,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick: () => void
|
||||
variant?: 'primary' | 'error'
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
}) {
|
||||
const baseColor = variant === 'error' ? '#E50045' : '#553DB8'
|
||||
const topColor = variant === 'error' ? '#FF1A5C' : '#7553FF'
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={disabled || loading}
|
||||
className="group w-full rounded-lg text-white cursor-pointer border-none p-0 transition-all disabled:cursor-not-allowed disabled:opacity-60"
|
||||
style={{ backgroundColor: baseColor }}
|
||||
>
|
||||
<span
|
||||
className="flex items-center justify-center gap-3 px-6 py-4 rounded-lg text-lg font-semibold transition-transform group-hover:-translate-y-1 group-active:translate-y-0 group-disabled:translate-y-0"
|
||||
style={{
|
||||
backgroundColor: topColor,
|
||||
border: `1px solid ${baseColor}`,
|
||||
}}
|
||||
>
|
||||
{loading && (
|
||||
<svg
|
||||
className="animate-spin h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{children}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// Feature Card Component
|
||||
function FeatureCard({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
icon: React.ReactNode
|
||||
title: string
|
||||
description: string
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-[#1C1825] rounded-xl p-4 border border-[#2D2640] hover:border-[#7553FF]/50 transition-all group">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="text-[#7553FF] group-hover:scale-110 transition-transform">
|
||||
{icon}
|
||||
</div>
|
||||
<h3 className="font-semibold text-white">{title}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-[#A49FB5] pl-9">{description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Result Badge Component
|
||||
function ResultBadge({
|
||||
type,
|
||||
spanOp,
|
||||
onCopy,
|
||||
}: {
|
||||
type: 'success' | 'error'
|
||||
spanOp: string
|
||||
onCopy: () => void
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(spanOp)
|
||||
setCopied(true)
|
||||
onCopy()
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
{type === 'error' && (
|
||||
<div className="flex items-center gap-2 bg-[#E50045]/10 border border-[#E50045]/30 rounded-lg px-4 py-3">
|
||||
<svg
|
||||
className="w-5 h-5 text-[#FF1A5C]"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<title>Error captured</title>
|
||||
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span className="text-[#FF1A5C] text-sm font-medium">
|
||||
Error captured and sent to Sentry
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === 'success' && (
|
||||
<div className="flex items-center gap-2 bg-[#00F261]/10 border border-[#00BF4D]/30 rounded-lg px-4 py-3">
|
||||
<svg
|
||||
className="w-5 h-5 text-[#00F261]"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<title>Trace complete</title>
|
||||
<path d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-[#00F261] text-sm font-medium">
|
||||
Trace completed successfully
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="relative flex items-center gap-2 bg-[#7553FF]/10 hover:bg-[#7553FF]/20 border border-[#7553FF]/30 rounded-lg px-4 py-2 transition-all cursor-pointer w-full"
|
||||
>
|
||||
<span className="text-[#B3A1FF] text-sm">span.op:</span>
|
||||
<code className="text-[#7553FF] font-mono text-sm">{spanOp}</code>
|
||||
<svg
|
||||
className="w-4 h-4 text-[#B3A1FF] ml-auto"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<title>Copy to clipboard</title>
|
||||
<path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
{copied && (
|
||||
<span className="absolute -top-8 left-1/2 -translate-x-1/2 bg-[#00F261] text-[#181423] text-xs font-medium px-2 py-1 rounded animate-pulse">
|
||||
Copied!
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Progress Bar Component
|
||||
function ProgressBar({ loading }: { loading: boolean }) {
|
||||
return (
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
<div
|
||||
className={`w-3 h-3 rounded-full transition-all ${loading ? 'bg-[#7553FF] animate-pulse' : 'bg-[#00F261]'}`}
|
||||
/>
|
||||
<div className="flex-1 h-2 bg-[#2D2640] rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-[#7553FF] to-[#B3A1FF] rounded-full transition-all duration-500"
|
||||
style={{ width: loading ? '60%' : '100%' }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-[#A49FB5] w-16 text-right">
|
||||
{loading ? 'Running...' : 'Complete'}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RouteComponent() {
|
||||
const [isLoading, setIsLoading] = useState<Record<string, boolean>>({})
|
||||
const [results, setResults] = useState<
|
||||
Record<string, { type: 'success' | 'error'; spanOp: string }>
|
||||
>({})
|
||||
const [sentryConfigured, setSentryConfigured] = useState<boolean | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// Check if Sentry DSN environment variable is set
|
||||
const hasDsn = !!import.meta.env.VITE_SENTRY_DSN
|
||||
setSentryConfigured(hasDsn)
|
||||
}, [])
|
||||
|
||||
// Don't show warning until we've checked on the client
|
||||
const showWarning = sentryConfigured === false
|
||||
|
||||
const handleClientError = async () => {
|
||||
setIsLoading((prev) => ({ ...prev, clientError: true }))
|
||||
try {
|
||||
await Sentry.startSpan(
|
||||
{ name: 'Client Error Flow Demo', op: 'demo.client-error' },
|
||||
async () => {
|
||||
Sentry.setContext('demo', {
|
||||
feature: 'client-error-demo',
|
||||
triggered_at: new Date().toISOString(),
|
||||
})
|
||||
throw new Error('Client-side error demonstration')
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
Sentry.captureException(error)
|
||||
setResults((prev) => ({
|
||||
...prev,
|
||||
clientError: { type: 'error', spanOp: 'demo.client-error' },
|
||||
}))
|
||||
} finally {
|
||||
setIsLoading((prev) => ({ ...prev, clientError: false }))
|
||||
}
|
||||
}
|
||||
|
||||
const handleServerError = async () => {
|
||||
setIsLoading((prev) => ({ ...prev, serverError: true }))
|
||||
try {
|
||||
await Sentry.startSpan(
|
||||
{ name: 'Server Error Flow Demo', op: 'demo.server-error' },
|
||||
async () => {
|
||||
Sentry.setContext('demo', {
|
||||
feature: 'server-error-demo',
|
||||
triggered_at: new Date().toISOString(),
|
||||
})
|
||||
await badServerFunc()
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
Sentry.captureException(error)
|
||||
setResults((prev) => ({
|
||||
...prev,
|
||||
serverError: { type: 'error', spanOp: 'demo.server-error' },
|
||||
}))
|
||||
} finally {
|
||||
setIsLoading((prev) => ({ ...prev, serverError: false }))
|
||||
}
|
||||
}
|
||||
|
||||
const handleClientTrace = async () => {
|
||||
setIsLoading((prev) => ({ ...prev, clientTrace: true }))
|
||||
await Sentry.startSpan(
|
||||
{ name: 'Client Operation', op: 'demo.client-trace' },
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
},
|
||||
)
|
||||
setResults((prev) => ({
|
||||
...prev,
|
||||
clientTrace: { type: 'success', spanOp: 'demo.client-trace' },
|
||||
}))
|
||||
setIsLoading((prev) => ({ ...prev, clientTrace: false }))
|
||||
}
|
||||
|
||||
const handleServerTrace = async () => {
|
||||
setIsLoading((prev) => ({ ...prev, serverTrace: true }))
|
||||
try {
|
||||
await Sentry.startSpan(
|
||||
{ name: 'Server Operation', op: 'demo.server-trace' },
|
||||
async () => {
|
||||
await goodServerFunc()
|
||||
},
|
||||
)
|
||||
setResults((prev) => ({
|
||||
...prev,
|
||||
serverTrace: { type: 'success', spanOp: 'demo.server-trace' },
|
||||
}))
|
||||
} finally {
|
||||
setIsLoading((prev) => ({ ...prev, serverTrace: false }))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen text-white"
|
||||
style={{
|
||||
fontFamily:
|
||||
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif',
|
||||
background:
|
||||
'linear-gradient(180deg, #181423 0%, #1C1825 50%, #181423 100%)',
|
||||
}}
|
||||
>
|
||||
<div className="max-w-5xl mx-auto px-6 py-16">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-flex items-center gap-4 mb-8">
|
||||
<div className="text-[#7553FF]">
|
||||
<SentryLogo size={56} />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight">
|
||||
Sentry Demo
|
||||
</h1>
|
||||
<p className="text-[#A49FB5] text-sm">
|
||||
Error monitoring & performance tracing
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-lg text-[#A49FB5] max-w-xl mx-auto leading-relaxed">
|
||||
Click the buttons below to trigger errors and traces, then view them
|
||||
in your{' '}
|
||||
<a
|
||||
href="https://sentry.io"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#7553FF] hover:text-[#B3A1FF] underline transition-colors"
|
||||
>
|
||||
Sentry dashboard
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Sentry Not Initialized Warning */}
|
||||
{showWarning && (
|
||||
<div className="mb-8 flex items-center gap-3 bg-[#E5A000]/10 border border-[#E5A000]/30 rounded-xl px-6 py-4">
|
||||
<svg
|
||||
className="w-6 h-6 text-[#E5A000] flex-shrink-0"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<title>Warning</title>
|
||||
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-[#E5A000] font-medium">
|
||||
Sentry is not initialized
|
||||
</p>
|
||||
<p className="text-[#A49FB5] text-sm mt-1">
|
||||
Set the{' '}
|
||||
<code className="bg-[#1C1825] px-1.5 py-0.5 rounded text-[#B3A1FF]">
|
||||
VITE_SENTRY_DSN
|
||||
</code>{' '}
|
||||
environment variable to enable error tracking and performance
|
||||
monitoring.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Features Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-12">
|
||||
<FeatureCard
|
||||
icon={
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
|
||||
</svg>
|
||||
}
|
||||
title="Error Monitoring"
|
||||
description="Client & server error tracking"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M13 3v18h-2V3h2zm6 6v12h-2V9h2zM7 14v7H5v-7h2z" />
|
||||
</svg>
|
||||
}
|
||||
title="Performance"
|
||||
description="Tracing and spans visualization"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z" />
|
||||
</svg>
|
||||
}
|
||||
title="Session Replay"
|
||||
description="Real user session playback"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z" />
|
||||
</svg>
|
||||
}
|
||||
title="Real-time Alerts"
|
||||
description="Instant issue notifications"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Testing Panels */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Client-Side Panel */}
|
||||
<div className="bg-[#1C1825] rounded-2xl p-8 border border-[#2D2640]">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-3 h-3 rounded-full bg-[#00F261]" />
|
||||
<h2 className="text-xl font-semibold">Client-Side Testing</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<SentryButton
|
||||
variant="error"
|
||||
onClick={handleClientError}
|
||||
loading={isLoading.clientError}
|
||||
disabled={sentryConfigured === false}
|
||||
>
|
||||
Trigger Client Error
|
||||
</SentryButton>
|
||||
{isLoading.clientError && (
|
||||
<ProgressBar loading={isLoading.clientError} />
|
||||
)}
|
||||
{results.clientError && !isLoading.clientError && (
|
||||
<ResultBadge
|
||||
type={results.clientError.type}
|
||||
spanOp={results.clientError.spanOp}
|
||||
onCopy={() => {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SentryButton
|
||||
variant="primary"
|
||||
onClick={handleClientTrace}
|
||||
loading={isLoading.clientTrace}
|
||||
disabled={sentryConfigured === false}
|
||||
>
|
||||
Test Client Trace
|
||||
</SentryButton>
|
||||
{isLoading.clientTrace && (
|
||||
<ProgressBar loading={isLoading.clientTrace} />
|
||||
)}
|
||||
{results.clientTrace && !isLoading.clientTrace && (
|
||||
<ResultBadge
|
||||
type={results.clientTrace.type}
|
||||
spanOp={results.clientTrace.spanOp}
|
||||
onCopy={() => {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Server-Side Panel */}
|
||||
<div className="bg-[#1C1825] rounded-2xl p-8 border border-[#2D2640]">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-3 h-3 rounded-full bg-[#7553FF]" />
|
||||
<h2 className="text-xl font-semibold">Server-Side Testing</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<SentryButton
|
||||
variant="error"
|
||||
onClick={handleServerError}
|
||||
loading={isLoading.serverError}
|
||||
disabled={sentryConfigured === false}
|
||||
>
|
||||
Trigger Server Error
|
||||
</SentryButton>
|
||||
{isLoading.serverError && (
|
||||
<ProgressBar loading={isLoading.serverError} />
|
||||
)}
|
||||
{results.serverError && !isLoading.serverError && (
|
||||
<ResultBadge
|
||||
type={results.serverError.type}
|
||||
spanOp={results.serverError.spanOp}
|
||||
onCopy={() => {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SentryButton
|
||||
variant="primary"
|
||||
onClick={handleServerTrace}
|
||||
loading={isLoading.serverTrace}
|
||||
disabled={sentryConfigured === false}
|
||||
>
|
||||
Test Server Trace
|
||||
</SentryButton>
|
||||
{isLoading.serverTrace && (
|
||||
<ProgressBar loading={isLoading.serverTrace} />
|
||||
)}
|
||||
{results.serverTrace && !isLoading.serverTrace && (
|
||||
<ResultBadge
|
||||
type={results.serverTrace.type}
|
||||
spanOp={results.serverTrace.spanOp}
|
||||
onCopy={() => {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Note */}
|
||||
<div className="mt-12 text-center">
|
||||
<p className="text-sm text-[#6E6C75]">
|
||||
This page uses{' '}
|
||||
<code className="bg-[#1C1825] px-2 py-1 rounded text-[#B3A1FF]">
|
||||
@sentry/tanstackstart-react
|
||||
</code>{' '}
|
||||
for full-stack error monitoring.
|
||||
<br />
|
||||
<a
|
||||
href="https://docs.sentry.io/platforms/javascript/guides/tanstackstart-react/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#7553FF] hover:text-[#B3A1FF] underline transition-colors"
|
||||
>
|
||||
Read the documentation →
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
45
src/routes/demo/tanstack-query.tsx
Normal file
45
src/routes/demo/tanstack-query.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
export const Route = createFileRoute('/demo/tanstack-query')({
|
||||
component: TanStackQueryDemo,
|
||||
})
|
||||
|
||||
function TanStackQueryDemo() {
|
||||
const { data } = useQuery({
|
||||
queryKey: ['todos'],
|
||||
queryFn: () =>
|
||||
Promise.resolve([
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
{ id: 3, name: 'Charlie' },
|
||||
]),
|
||||
initialData: [],
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-center min-h-screen bg-gradient-to-br from-purple-100 to-blue-100 p-4 text-white"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'radial-gradient(50% 50% at 95% 5%, #f4a460 0%, #8b4513 70%, #1a0f0a 100%)',
|
||||
}}
|
||||
>
|
||||
<div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10">
|
||||
<h1 className="text-2xl mb-4">
|
||||
TanStack Query Simple Promise Handling
|
||||
</h1>
|
||||
<ul className="mb-4 space-y-2">
|
||||
{data.map((todo) => (
|
||||
<li
|
||||
key={todo.id}
|
||||
className="bg-white/10 border border-white/20 rounded-lg p-3 backdrop-blur-sm shadow-md"
|
||||
>
|
||||
<span className="text-lg text-white">{todo.name}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
120
src/routes/index.tsx
Normal file
120
src/routes/index.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Button } from "@heroui/react"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import {
|
||||
Route as RouteIcon,
|
||||
Server,
|
||||
Shield,
|
||||
Sparkles,
|
||||
Waves,
|
||||
Zap,
|
||||
} from "lucide-react"
|
||||
|
||||
export const Route = createFileRoute("/")({ component: App })
|
||||
|
||||
function App() {
|
||||
const features = [
|
||||
{
|
||||
icon: <Zap className="w-12 h-12 text-cyan-400" />,
|
||||
title: "Powerful Server Functions",
|
||||
description:
|
||||
"Write server-side code that seamlessly integrates with your client components. Type-safe, secure, and simple.",
|
||||
},
|
||||
{
|
||||
icon: <Server className="w-12 h-12 text-cyan-400" />,
|
||||
title: "Flexible Server Side Rendering",
|
||||
description:
|
||||
"Full-document SSR, streaming, and progressive enhancement out of the box. Control exactly what renders where.",
|
||||
},
|
||||
{
|
||||
icon: <RouteIcon className="w-12 h-12 text-cyan-400" />,
|
||||
title: "API Routes",
|
||||
description:
|
||||
"Build type-safe API endpoints alongside your application. No separate backend needed.",
|
||||
},
|
||||
{
|
||||
icon: <Shield className="w-12 h-12 text-cyan-400" />,
|
||||
title: "Strongly Typed Everything",
|
||||
description:
|
||||
"End-to-end type safety from server to client. Catch errors before they reach production.",
|
||||
},
|
||||
{
|
||||
icon: <Waves className="w-12 h-12 text-cyan-400" />,
|
||||
title: "Full Streaming Support",
|
||||
description:
|
||||
"Stream data from server to client progressively. Perfect for AI applications and real-time updates.",
|
||||
},
|
||||
{
|
||||
icon: <Sparkles className="w-12 h-12 text-cyan-400" />,
|
||||
title: "Next Generation Ready",
|
||||
description:
|
||||
"Built from the ground up for modern web applications. Deploy anywhere JavaScript runs.",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900">
|
||||
<Button> Hola</Button>
|
||||
<section className="relative py-20 px-6 text-center overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-cyan-500/10 via-blue-500/10 to-purple-500/10"></div>
|
||||
<div className="relative max-w-5xl mx-auto">
|
||||
<div className="flex items-center justify-center gap-6 mb-6">
|
||||
<img
|
||||
src="/tanstack-circle-logo.png"
|
||||
alt="TanStack Logo"
|
||||
className="w-24 h-24 md:w-32 md:h-32"
|
||||
/>
|
||||
<h1 className="text-6xl md:text-7xl font-black text-white [letter-spacing:-0.08em]">
|
||||
<span className="text-gray-300">TANSTACK</span>{" "}
|
||||
<span className="bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent">
|
||||
START
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-2xl md:text-3xl text-gray-300 mb-4 font-light">
|
||||
The framework for next generation AI applications
|
||||
</p>
|
||||
<p className="text-lg text-gray-400 max-w-3xl mx-auto mb-8">
|
||||
Full-stack framework powered by TanStack Router for React and Solid.
|
||||
Build modern applications with server functions, streaming, and type
|
||||
safety.
|
||||
</p>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<a
|
||||
href="https://tanstack.com/start"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-8 py-3 bg-cyan-500 hover:bg-cyan-600 text-white font-semibold rounded-lg transition-colors shadow-lg shadow-cyan-500/50"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
<p className="text-gray-400 text-sm mt-2">
|
||||
Begin your TanStack Start journey by editing{" "}
|
||||
<code className="px-2 py-1 bg-slate-700 rounded text-cyan-400">
|
||||
/src/routes/index.tsx
|
||||
</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-16 px-6 max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{features.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-slate-800/50 backdrop-blur-sm border border-slate-700 rounded-xl p-6 hover:border-cyan-500/50 transition-all duration-300 hover:shadow-lg hover:shadow-cyan-500/10"
|
||||
>
|
||||
<div className="mb-4">{feature.icon}</div>
|
||||
<h3 className="text-xl font-semibold text-white mb-3">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-gray-400 leading-relaxed">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
139
src/styles.css
Normal file
139
src/styles.css
Normal file
@@ -0,0 +1,139 @@
|
||||
@import 'tailwindcss';
|
||||
@import "@heroui/styles";
|
||||
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
body {
|
||||
@apply m-0;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
||||
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family:
|
||||
source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.871 0.006 286.286);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.871 0.006 286.286);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.141 0.005 285.823);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.141 0.005 285.823);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.396 0.141 25.723);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.274 0.006 286.033);
|
||||
--input: oklch(0.274 0.006 286.033);
|
||||
--ring: oklch(0.442 0.017 285.786);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(0.274 0.006 286.033);
|
||||
--sidebar-ring: oklch(0.442 0.017 285.786);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--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-destructive-foreground: var(--destructive-foreground);
|
||||
--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);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--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;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user