diff --git a/.agents/analytics.md b/.agents/analytics.md deleted file mode 100644 index bcd12c824..000000000 --- a/.agents/analytics.md +++ /dev/null @@ -1,375 +0,0 @@ -# Analytics - -Reference for the TanStack.com event taxonomy in Google Analytics 4. - -**GA4 property:** `G-JMT1Z50SPS` -**First-party proxy:** all hits route through `/_a/g/collect` on tanstack.com (see `netlify.toml`) -**Implementation:** [src/utils/analytics.ts](../src/utils/analytics.ts), [src/utils/analytics/events.ts](../src/utils/analytics/events.ts) - -## Design principles - -1. **Event names describe what happened.** Properties carry context. Adding a new partner placement is an enum value, not a new event. -2. **One event per outcome, not per intent.** No `_clicked`/`_attempted` events when an outcome event is going to fire anyway. -3. **No per-row events.** Counts and joined-string arrays as properties, not N rows per generation. -4. **Session context propagates.** Slow-changing props like `mode_used` and `idea_used` are stamped on every builder event so any breakdown works without joins. -5. **Typed registry.** Wrong props for an event = TypeScript error, not silent bad data. - ---- - -## The funnel - -Application Builder, four steps. No `OR` conditions, no overlapping event names. - -| # | Step | Event | Filter | -| --- | --------------------- | ------------------- | --------------------------------- | -| 1 | Landed on builder | `page_view` | `page_type = application_builder` | -| 2 | Got an analysis | `builder_analyzed` | — | -| 3 | Got a generation | `builder_generated` | — | -| 4 | Took action on result | `builder_activated` | — | - -Drop-off between any two steps is unambiguous. - -**Failure rates** are separate explorations, not branches off the funnel: - -- Analysis failure rate = `builder_failed[stage=analysis]` ÷ (`builder_analyzed` + `builder_failed[stage=analysis]`) -- Generation failure rate = `builder_failed[stage=generation]` ÷ (`builder_generated` + `builder_failed[stage=generation]`) -- Login wall rate = `builder_failed[stage=login_blocked]` ÷ `page_view[page_type=application_builder]` - ---- - -## Event reference (9 events) - -Every event automatically receives `page_location`, `page_path`, `page_title`, `page_type` from the analytics utility. - -### `page_view` - -Fires on initial load (auto from gtag config) and on every SPA navigation. - -| Prop | Type | Notes | -| --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ | -| `page_location` | string | Full URL | -| `page_path` | string | Pathname | -| `page_title` | string | `document.title` | -| `page_type` | enum | `home`, `partners_index`, `partner_detail`, `blog_index`, `blog_post`, `docs`, `partners_embed`, `application_builder`, `page` | - ---- - -### `partner_viewed` - -A partner UI element scrolled ≥50% into the viewport. Fires once per element-mount per session (the underlying `IntersectionObserver` disconnects after first fire). - -| Prop | Type | Notes | -| ------------ | ------- | --------------------------------------------------- | -| `partner_id` | string | Stable partner identifier | -| `placement` | enum | See `PartnerPlacement` below | -| `slot_index` | number? | Position in the surface (0-indexed) when applicable | - -**Note on inflation:** when filters change on the partners directory, cards unmount/remount and `partner_viewed` re-fires. Treat session-unique impressions as the dedup'd metric (compute in BigQuery with `FIRST_VALUE(... PARTITION BY session_id, partner_id)`). - ---- - -### `partner_clicked` - -User clicked a partner UI element to navigate somewhere. - -| Prop | Type | Notes | -| ------------------ | ------- | -------------------------------------------------------------------------- | -| `partner_id` | string | | -| `placement` | enum | See `PartnerPlacement` below | -| `destination` | enum | `external` (partner's site) or `internal_detail` (our partner detail page) | -| `destination_host` | string? | Host of the destination URL when `external` | -| `slot_index` | number? | Position in the surface when applicable | - -CTR per placement = `partner_clicked` ÷ `partner_viewed` filtered to same `placement`. - ---- - -### `partner_filter_applied` - -User changed the filter state on the partners directory. - -| Prop | Type | Notes | -| ----------------- | -------------- | --------------------------------------------------------------------------------------------- | -| `change` | enum | `libraries_changed`, `status_changed`, `cleared_all` | -| `library_filters` | string | Comma-joined library ids in the filter, or empty string | -| `status_filter` | string \| null | `null` means "no status filter applied" — distinguishes from "user explicitly chose 'active'" | -| `result_count` | number | Number of partners visible after the filter | - ---- - -### `partner_inquiry_started` - -User clicked a "get in touch", "let's chat", or "become a partner" CTA. - -| Prop | Type | Notes | -| ----------- | ---- | ---------------------------------------------------------- | -| `placement` | enum | `partners_index_cta`, `library_callout`, `docs_right_rail` | - ---- - -### `builder_analyzed` - -Analysis API call succeeded. Outcome event — always preceded by user intent, no separate `_requested` event. - -| Prop | Type | Notes | -| ------------------------ | ------- | --------------------------------------------- | -| `mode_used` | enum | Session context: `lucky`, `confident`, `none` | -| `idea_used` | string | Session context: idea label, or `none` | -| `analysis_deployment` | string? | Inferred deploy target | -| `inferred_library_count` | number | | -| `inferred_partner_count` | number | | -| `feature_count` | number | | - ---- - -### `builder_generated` - -Generation API call succeeded. - -| Prop | Type | Notes | -| ----------------------- | ------- | ---------------------------------------------------------------------- | -| `mode_used` | enum | Session context | -| `idea_used` | string | Session context | -| `final_deployment` | string? | The chosen deploy target on the final result | -| `final_package_manager` | string | `pnpm`, `npm`, `yarn`, `bun` | -| `final_library_count` | number | | -| `final_partner_count` | number | | -| `final_addon_count` | number | | -| `library_ids` | string | Comma-joined LibraryIds — use `SPLIT()` in BigQuery for top-N analysis | -| `partner_ids` | string | Comma-joined | -| `addon_ids` | string | Comma-joined | - ---- - -### `builder_failed` - -Single umbrella event for analysis failures, generation failures, and login-wall blocks. Use the `stage` prop to distinguish. - -| Prop | Type | Notes | -| --------------------------------- | ------- | -------------------------------------------------------------------------------------------- | -| `mode_used` | enum | Session context | -| `idea_used` | string | Session context | -| `stage` | enum | `analysis`, `generation`, `login_blocked` | -| `error_message` | string? | Free-form error message — high cardinality, don't register as a dimension; query in BigQuery | -| `retry_after` | number? | Seconds until retry permitted (login_blocked only) | -| `anonymous_generations_remaining` | number? | When the failure was rate-limit related | - ---- - -### `builder_activated` - -User took an action on the generated result. Single event with `action` prop covers all post-generation actions. - -| Prop | Type | Notes | -| ----------- | ------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `mode_used` | enum | Session context | -| `idea_used` | string | Session context | -| `action` | enum | See `BuilderAction` below | -| `surface` | enum | `result_panel` (main builder UI) or `deploy_dialog` | -| `provider` | string? | Deploy provider when applicable: `vercel`, `netlify`, `cloudflare` | -| `automatic` | boolean | `true` for system-driven actions (e.g., deploy_dialog auto-redirect countdown). Filter to `false` for true user click rates. | - -**Important:** automatic prompt-copies that fire as a side-effect of generation do NOT emit `builder_activated`. Only user-driven actions count as activation. - ---- - -## Enums - -### `PartnerPlacement` - -| Value | Where it appears | -| -------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -| `directory` | Partner cards in `/partners` index | -| `detail` | Partner detail page CTA | -| `docs_rail` | Right rail on docs pages — partner cards AND the "Become a Partner" link both fire with this placement (event name distinguishes) | -| `blog_rail` | Right rail on blog pages | -| `grid` | Generic partners grid (fallback) | -| `home_grid` | Home page social-proof grid | -| `library_grid` | Library page partners section — filter `page_path` to know which library | -| `embed_grid` | Partners embed view | -| `docs_strip` | Mobile partner strip in docs | -| `ecosystem_game` | 3D ecosystem game islands | -| `partners_index_cta` | "Get in touch" mailto on `/partners` | -| `library_callout` | "Let's chat" callout per library | - -### `BuilderAction` - -| Value | Means | -| -------------------------- | ---------------------------------------------------------------------- | -| `copy_prompt` | User clicked a copy button on the prompt | -| `deploy` | Started a deploy through the deploy dialog | -| `clone_repo` | Cloned the GitHub repo | -| `open_codex` | Opened the result in Codex | -| `open_claude` | Opened the result in Claude | -| `open_cursor` | Opened the result in Cursor | -| `download` | Downloaded the project as a zip | -| `open_advanced` | Opened the advanced builder editor | -| `netlify_start` | Started a Netlify deploy from the result | -| `provider_redirect_manual` | User clicked through to deploy provider | -| `provider_redirect_auto` | Countdown auto-redirected user to deploy provider (`automatic = true`) | -| `open_repo` | Opened the project repo from the deploy dialog | - ---- - -## Session context - -`mode_used` and `idea_used` are tracked in the builder hook and stamped on **every** builder event. This means any builder event can be sliced by mode or idea without session joins. - -**`mode_used`** transitions: - -- `none` → `lucky` when user clicks "I'm feeling lucky" -- `none` → `confident` when user clicks "I'm feeling confident" - -**`idea_used`** transitions: - -- `none` → idea label string when user picks a suggested idea -- Reset back to `none` if user clears or types fresh input (TBD — currently sticks for the session) - ---- - -## Custom dimensions to register in GA4 - -Admin → Custom definitions → Create custom dimension. **Event scope** for all of these. Without registration, they're stored in BigQuery export but invisible in the GA4 UI. - -| Dimension name | API name | Used on | -| --------------------- | ----------------------- | -------------------------------------------------------- | -| Placement | `placement` | partner_viewed, partner_clicked, partner_inquiry_started | -| Partner ID | `partner_id` | partner_viewed, partner_clicked | -| Mode used | `mode_used` | all builder events | -| Idea used | `idea_used` | all builder events | -| Action | `action` | builder_activated | -| Surface | `surface` | builder_activated | -| Stage | `stage` | builder_failed | -| Final deployment | `final_deployment` | builder_generated | -| Final package manager | `final_package_manager` | builder_generated | -| Final library count | `final_library_count` | builder_generated | -| Final partner count | `final_partner_count` | builder_generated | -| Page type | `page_type` | all events | - -12 dimensions. Well under the 50-dimension event-scoped limit. - -**Don't register:** `error_message`, `library_ids`, `partner_ids`, `addon_ids`, `destination_host`. High cardinality. Query in BigQuery. - ---- - -## Common GA4 queries - -### "What's our funnel completion rate?" - -**Explore → Funnel exploration**, four steps as defined above. Open funnel. Show elapsed time. - -### "Lucky vs Confident: which mode converts better?" - -Same funnel, breakdown dropdown = `mode_used`. Compare side by side. - -### "Which deploy targets do successful generations land on?" - -**Explore → Free-form**, dimension = `final_deployment`, metric = event count of `builder_generated`. - -### "What % of users hit the login wall?" - -Free-form, filter `event_name = builder_failed`, breakdown by `stage`. - -### "Which placement converts best for partner discovery?" - -Free-form, filter `event_name = partner_clicked OR partner_viewed`, breakdown by `placement`. Compute CTR yourself: clicks ÷ views per placement. - -### "Top 10 libraries in completed generations" - -Requires BigQuery — `library_ids` is high-cardinality. Sample query: - -```sql -SELECT - library_id, - COUNT(*) AS generations -FROM `tanstack.analytics_*.events_*`, -UNNEST(SPLIT((SELECT value.string_value - FROM UNNEST(event_params) - WHERE key = 'library_ids'), ',')) AS library_id -WHERE event_name = 'builder_generated' - AND _TABLE_SUFFIX BETWEEN '20260101' AND '20260131' -GROUP BY library_id -ORDER BY generations DESC -LIMIT 10 -``` - ---- - -## BigQuery export - -**Strongly recommended.** Free at our event volume. Without it, anything beyond stock reports is painful. - -Setup: - -1. GA4 Admin → BigQuery Links → Link -2. Pick a GCP project, daily export, US multi-region -3. Tables appear at `tanstack.analytics_.events_YYYYMMDD` after ~24h - -Once enabled, all event properties are queryable — including the ones not registered as custom dimensions. Use SQL for everything dimensional or aggregate-heavy. Use the GA4 UI for funnel exploration and headline numbers. - ---- - -## Adding new events - -Anything additive should not require schema migration of the existing taxonomy. - -**Add a new partner placement:** - -1. Add the value to `PartnerPlacement` in [src/utils/analytics/events.ts](../src/utils/analytics/events.ts) -2. Use it at the call site -3. (Optional) update the placement table in this doc - -**Add a new builder action:** - -1. Add the value to `BuilderAction` -2. Use it at the `builder_activated` call site - -**Add a new event:** - -1. Add a new union member to `AnalyticsEvent` with its prop interface -2. Call `trackEvent({ name: '...', props: { ... } })` -3. Register relevant breakdown props as custom dimensions in GA4 admin -4. Document the event in this file - -**Don't:** add new properties to existing events without updating both the type definition and this doc. Schema drift in analytics events is the slowest bug to detect. - ---- - -## Code locations - -| File | Purpose | -| ------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | -| [src/utils/analytics.ts](../src/utils/analytics.ts) | `trackEvent`, `useTrackedImpression`, `trackPageView`, `getPageType` | -| [src/utils/analytics/events.ts](../src/utils/analytics/events.ts) | Typed event registry — discriminated union of all events | -| [src/utils/analytics/providers/google.ts](../src/utils/analytics/providers/google.ts) | gtag wrapper | -| [src/utils/analytics/types.ts](../src/utils/analytics/types.ts) | Provider interface | -| [src/routes/\_\_root.tsx](../src/routes/__root.tsx) | gtag bootstrap, `PageViewTracker` | -| [netlify.toml](../netlify.toml) | First-party proxy redirects | - ---- - -## Migration history - -### 2026-05 — schema v2 - -Collapsed 28 events into 9. Removed `application_starter_*` event family. Replaced with `builder_*` events. Mode and idea selection moved from standalone events into session-context props on every builder event. - -**Events removed entirely** (no replacement): - -- `application_starter_library_toggled`, `_integration_toggled`, `_package_manager_toggled`, `_toolchain_toggled` — config exploration depth no longer tracked. Final config is on `builder_generated`. -- `application_starter_continue_clicked`, `_generate_clicked` — intent implied by outcome events. -- `application_starter_login_clicked`, `_value_copied` (auto), `_builder_result_applied`, `_final_partner_in_prompt`, `_final_addon_in_prompt`. - -**Events folded into others:** - -- `application_starter_action_clicked` (mode_selected) → `mode_used` prop on subsequent events -- `application_starter_idea_selected` → `idea_used` prop on subsequent events -- `application_starter_login_required` → `builder_failed[stage=login_blocked]` -- `application_starter_value_copied` (user trigger) → `builder_activated[action=copy_prompt]` -- All other `application_starter_action_clicked` calls → `builder_activated` -- `partner_card_clicked`, `partner_click` → `partner_clicked` -- `partner_impression`, `partner_detail_viewed` → `partner_viewed` -- `partners_filter_changed` → `partner_filter_applied` -- `partner_inquiry_clicked`, `become_partner_clicked` → `partner_inquiry_started` - -Historical data with old event names is still queryable in GA4 and BigQuery. Cutover date: see git log for the migration commit. diff --git a/.agents/index.md b/.agents/index.md deleted file mode 100644 index 01316aeb9..000000000 --- a/.agents/index.md +++ /dev/null @@ -1,19 +0,0 @@ -# Agent Guidelines - -TanStack.com marketing site built with TanStack Start. - -## Essentials - -- Package manager: `pnpm` -- Run `pnpm test` before commits not after every tiny edit -- Don't run builds or tests after every change. This is a visual site; assume changes work unless reported otherwise. -- Rely on `tsc` or your built-in LSP integration (hopefully you have this) -- **Typesafety is paramount.** Never cast types; fix at source instead. See [typescript.md](./typescript.md). - -## Topic Guides - -- [TypeScript Conventions](./typescript.md): Type inference, casting rules, generic naming -- [TanStack Patterns](./tanstack-patterns.md): Loaders, server functions, environment shaking -- [UI Style Guide](./ui-style.md): Visual design principles for 2026 -- [Workflow](./workflow.md): Build commands, debugging, Playwright -- [Analytics](./analytics.md): GA4 event taxonomy, funnel definition, custom dimensions diff --git a/.agents/tanstack-patterns.md b/.agents/tanstack-patterns.md deleted file mode 100644 index d0ff4e278..000000000 --- a/.agents/tanstack-patterns.md +++ /dev/null @@ -1,76 +0,0 @@ -# TanStack Patterns - -## loaderDeps Must Be Specific - -Only include properties actually used in the loader. This ensures proper cache invalidation. - -```typescript -// Bad: includes everything -loaderDeps: ({ search }) => search, -loader: async ({ deps }) => { - await fetchData({ page: deps.page, pageSize: deps.pageSize }) -} - -// Good: only what's used -loaderDeps: ({ search }) => ({ - page: search.page, - pageSize: search.pageSize, -}), -loader: async ({ deps }) => { - await fetchData({ page: deps.page, pageSize: deps.pageSize }) -} -``` - -## Loaders Are Isomorphic - -Loaders run on both server and client. They cannot directly access server-only APIs. - -```typescript -// Bad: direct server API access -loader: async () => { - const data = await fs.readFile('data.json') - return { data } -} - -// Good: call a server function -loader: async () => { - const data = await serverFn({ data: { id: '123' } }) - return { data } -} -``` - -## Environment Shaking - -TanStack Start strips any code not referenced by a `createServerFn` handler from the client build. - -- Server-only code (database, fs) is automatically excluded from client bundles -- Only code inside `createServerFn` handlers goes to server bundles -- Code outside handlers is included in both bundles - -## Importing Server Functions - -Server functions wrapped in `createServerFn` can be imported statically. Never use dynamic imports for server-only code in components. - -```typescript -// Bad: dynamic import causes bundler issues -const rolesQuery = useQuery({ - queryFn: async () => { - const { listRoles } = await import('~/utils/roles.server') - return listRoles({ data: {} }) - }, -}) - -// Good: static import -import { listRoles } from '~/utils/roles.server' - -const rolesQuery = useQuery({ - queryFn: async () => listRoles({ data: {} }), -}) -``` - -## Server-Only Import Rules - -1. `createServerFn` wrappers can be imported statically anywhere -2. Direct server-only code (database clients, fs) must only be imported: - - Inside `createServerFn` handlers - - In `*.server.ts` files diff --git a/.agents/typescript.md b/.agents/typescript.md deleted file mode 100644 index e63301ec4..000000000 --- a/.agents/typescript.md +++ /dev/null @@ -1,45 +0,0 @@ -# TypeScript Conventions - -## Avoid Type Casting - -Never cast types unless absolutely necessary. This includes: - -- Manual generic type parameters (e.g., ``) -- Type assertions using `as` -- Type assertions using `satisfies` - -## Prefer Type Inference - -Infer types by going up the logical chain: - -1. **Schema validation** as source of truth (Convex, Zod, etc.) -2. **Type inference** from function return types, API responses -3. **Fix at source** (schema, API definition, function signature) rather than casting at point of use - -```typescript -// Bad -const result = api.getData() as MyType -const value = getValue() - -// Good -const result = api.getData() // Type inferred from return type -const value = getValue() // Type inferred from implementation -``` - -## Generic Type Parameter Naming - -All generic type parameters must be prefixed with `T`. - -```typescript -// Bad -function withCapability( - handler: (user: AuthUser, ...args: Args) => R, -) { ... } - -// Good -function withCapability( - handler: (user: AuthUser, ...args: TArgs) => TReturn, -) { ... } -``` - -Common names: `T`, `TArgs`, `TReturn`, `TData`, `TError`, `TKey`, `TValue` diff --git a/.agents/ui-style.md b/.agents/ui-style.md deleted file mode 100644 index d696b3346..000000000 --- a/.agents/ui-style.md +++ /dev/null @@ -1,52 +0,0 @@ -# UI Style Guide 2026 - -## Layout - -- Fewer, well-defined containers over many small sections -- Generous spacing creates separation before adding visual effects -- Cards are acceptable when they express grouping or hierarchy - -## Corners - -- Rounded corners are standard -- Subtle radius values that feel intentional, not playful -- Avoid sharp 90-degree corners unless intentionally industrial - -## Shadows and Depth - -- Soft, low-contrast, diffused shadows -- Shadows imply separation, not elevation theatrics -- No heavy drop shadows or strong directional lighting -- One to two shadow layers max - -## Cards - -- Cards should feel grounded, not floating -- Light elevation, border plus shadow, or surface contrast -- Don't overuse cards as a default layout primitive - -## Color and Surfaces - -- Soft neutrals, off-whites, warm grays -- Surface contrast or translucency instead of strong outlines -- Glass/frosted effects acceptable when subtle and accessible - -## Interaction - -- Micro transitions reinforce spatial relationships -- Hover/focus states feel responsive, not animated -- No excessive motion or springy effects - -## Typography - -- Strong headings, calm body text -- No visual noise around content - -## What to Avoid - -- Chunky shadows -- Overly flat, sterile layouts -- Neumorphism as primary style -- Over-designed card grids - -**Summary: If depth does not improve comprehension, remove it.** diff --git a/.agents/workflow.md b/.agents/workflow.md deleted file mode 100644 index 07838ff50..000000000 --- a/.agents/workflow.md +++ /dev/null @@ -1,46 +0,0 @@ -# Workflow - -## Build Commands - -- `pnpm test`: Run at end of task batches -- `pnpm build`: Only for build/bundler issues or verifying production output -- `pnpm lint`: Check for code issues -- `dev` runs indefinitely in watch mode - -Don't build after every change. This is a visual site; assume changes work. - -## Dev Authentication - -The dev server uses the real production database and OAuth. There are no auth bypasses. - -To authenticate a dev session, the human must run: - -```sh -pnpm auth:login -``` - -This opens `tanstack.com`, completes OAuth, and saves `DEV_SESSION_TOKEN` to `.env.local`. The dev server then auto-injects that token as a session cookie on startup. - -If the user asks for help with anything requiring authentication (account pages, admin, mutations) and `DEV_SESSION_TOKEN` is not set in `.env.local`, tell them to run `pnpm auth:login` first and restart the dev server. - -## Debugging Visual Issues - -When something doesn't work or look right: - -1. Use Playwright MCP to view the page and debug visually -2. Use `pnpm build` only for build/bundler issues -3. Use `pnpm lint` for code issues - -## Playwright - -Playwright is available via `npx playwright` and should be used freely to interact with the running dev server like a human would. This includes testing, debugging, visual verification, design iteration, and general development workflows. - -Since the dev server runs with a real authenticated session (via `DEV_SESSION_TOKEN`), Playwright has full access to gated pages, account features, admin areas, and authenticated mutations — exactly as the signed-in user would. - -Common uses: - -- `npx playwright screenshot ` — capture current state of any page -- Navigate to a page, interact with elements, re-screenshot to verify changes -- Debug layout/visual issues without needing a human to look -- Verify authenticated flows end-to-end (account, showcase submissions, admin, etc.) -- Iterate on UI by screenshotting, editing, screenshotting again diff --git a/.claude/launch.json b/.claude/launch.json deleted file mode 100644 index 7e34cf155..000000000 --- a/.claude/launch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "0.0.1", - "configurations": [ - { - "name": "web", - "runtimeExecutable": "pnpm", - "runtimeArgs": ["dev"], - "port": 3000 - }, - { - "name": "db-studio", - "runtimeExecutable": "pnpm", - "runtimeArgs": ["db:studio"], - "port": 4983 - } - ] -} diff --git a/.claude/tanstack-patterns.md b/.claude/tanstack-patterns.md deleted file mode 120000 index 7335c6c22..000000000 --- a/.claude/tanstack-patterns.md +++ /dev/null @@ -1 +0,0 @@ -../.agents/tanstack-patterns.md \ No newline at end of file diff --git a/.claude/typescript.md b/.claude/typescript.md deleted file mode 120000 index bf4e25139..000000000 --- a/.claude/typescript.md +++ /dev/null @@ -1 +0,0 @@ -../.agents/typescript.md \ No newline at end of file diff --git a/.claude/ui-style.md b/.claude/ui-style.md deleted file mode 120000 index f59b7412f..000000000 --- a/.claude/ui-style.md +++ /dev/null @@ -1 +0,0 @@ -../.agents/ui-style.md \ No newline at end of file diff --git a/.claude/workflow.md b/.claude/workflow.md deleted file mode 120000 index c19e63b75..000000000 --- a/.claude/workflow.md +++ /dev/null @@ -1 +0,0 @@ -../.agents/workflow.md \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..af7fa28f8 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,5 @@ +{ + "extends": ["react-app"], + "parser": "@typescript-eslint/parser", + "plugins": ["react-hooks"] +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index fd99b3b17..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: tannerlinsley diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml deleted file mode 100644 index b336e672a..000000000 --- a/.github/workflows/autofix.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: autofix.ci # needed to securely identify the workflow - -on: - pull_request: - push: - branches: [main] - -concurrency: - group: ${{ github.workflow }}-${{ github.event.number || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - autofix: - name: autofix - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - persist-credentials: false - - name: Setup Tools - uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main - - name: Fix formatting - run: pnpm format - - name: Apply fixes - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 - with: - commit-message: 'ci: apply automated fixes' diff --git a/.github/workflows/docs-links.yml b/.github/workflows/docs-links.yml deleted file mode 100644 index 4771fdd25..000000000 --- a/.github/workflows/docs-links.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Docs Links - -on: - workflow_dispatch: - schedule: - - cron: '0 9 * * *' - -permissions: - contents: read - -jobs: - docs-links: - name: Docs Links - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Setup Tools - uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main - - - name: Check docs menu links - env: - GITHUB_TOKEN: ${{ github.token }} - run: | - set +e - pnpm docs:links:check -- --json docs-menu-link-report.json - status=$? - - if [ -f docs-menu-link-report.json ]; then - { - echo "### Docs menu link report" - echo - echo '```json' - cat docs-menu-link-report.json - echo '```' - } >> "$GITHUB_STEP_SUMMARY" - fi - - exit $status diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml new file mode 100644 index 000000000..0a534520b --- /dev/null +++ b/.github/workflows/pr.yaml @@ -0,0 +1,25 @@ +name: PR + +on: + pull_request: + +jobs: + pr: + name: PR + runs-on: ubuntu-latest + steps: + - name: Git Checkout + uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: pnpm + - name: Install Packages + run: pnpm install --frozen-lockfile + - name: Run Lint + run: pnpm lint + - name: Run Build + run: pnpm build diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml deleted file mode 100644 index d4497f807..000000000 --- a/.github/workflows/pr.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: PR - -on: - pull_request: - -permissions: - contents: read - -jobs: - pr: - name: PR - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - persist-credentials: false - - name: Setup Tools - uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main - - name: Run Build - run: pnpm build - - name: Run Tests - run: pnpm test diff --git a/.github/workflows/update-tanstack-deps.yml b/.github/workflows/update-tanstack-deps.yml deleted file mode 100644 index 7d4ad61d9..000000000 --- a/.github/workflows/update-tanstack-deps.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Update TanStack Dependencies - -on: - workflow_dispatch: - schedule: - # Run weekly on Sundays at 10:00 AM UTC - - cron: '0 10 * * 0' - -jobs: - update-deps: - name: Update TanStack Dependencies - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Git Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - # This scheduled job commits dependency updates back to the branch. - persist-credentials: true - - - name: Setup pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 - - - name: Setup Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version-file: .nvmrc - cache: pnpm - - - name: Install Packages - run: pnpm install --frozen-lockfile - - - name: Update TanStack Dependencies - run: pnpm up "@tanstack/*" --latest - - - name: Check for Changes - id: git-check - run: | - git status --porcelain . - if [[ -z $(git status --porcelain .) ]]; then - echo "No changes detected" - echo "changes=false" >> $GITHUB_OUTPUT - else - echo "Changes detected" - echo "changes=true" >> $GITHUB_OUTPUT - fi - - - name: Run Lint - if: steps.git-check.outputs.changes == 'true' - run: pnpm lint - - - name: Run Build - if: steps.git-check.outputs.changes == 'true' - run: pnpm build - - - name: Commit and Push Changes - if: steps.git-check.outputs.changes == 'true' - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - git add . - git commit -m "chore: update @tanstack/* dependencies" - git push diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml deleted file mode 100644 index 1d4088db8..000000000 --- a/.github/workflows/zizmor.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: GitHub Actions Security Analysis - -on: - push: - branches: [main] - pull_request: - branches: ['**'] - -permissions: {} - -jobs: - zizmor: - name: Run zizmor - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Run zizmor - uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 - with: - advanced-security: false - annotations: true diff --git a/.gitignore b/.gitignore index 7021fe7e7..2b6f57077 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,14 @@ node_modules package-lock.json yarn.lock -drizzle/migrations -.tanstack .DS_Store .cache .env -.env.* .vercel .output .vinxi -.tanstack-start/build -.nitro/* -.netlify/* -.wrangler/* +.netlify /build/ /api/ @@ -26,14 +20,3 @@ drizzle/migrations dist .vscode/ .env.local - -# Content Collections generated files -.content-collections - -test-results -.claude/CLAUDE.md -.claude/worktrees -.eslintcache -.tsbuildinfo -src/routeTree.gen.ts -.og-preview/ diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index 63860b5bd..000000000 --- a/.husky/pre-commit +++ /dev/null @@ -1 +0,0 @@ -pnpm husky diff --git a/.nvmrc b/.nvmrc index d9b042470..c12134be3 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v25.9.0 +v20.15.0 diff --git a/.oxfmtrc.json b/.oxfmtrc.json deleted file mode 100644 index ac154e249..000000000 --- a/.oxfmtrc.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "./node_modules/oxfmt/configuration_schema.json", - "semi": false, - "singleQuote": true, - "trailingComma": "all", - "printWidth": 80, - "sortPackageJson": false, - "ignorePatterns": [ - "**/api", - "**/build", - "**/public", - "pnpm-lock.yaml", - "routeTree.gen.ts", - "src/blog/tanstack-db-0.1-the-embedded-client-database-for-tanstack-query.md", - ".content-collections", - ".claude", - "dist/**", - ".output/**" - ] -} diff --git a/.oxlintrc.json b/.oxlintrc.json deleted file mode 100644 index f6467d88c..000000000 --- a/.oxlintrc.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "$schema": "./node_modules/oxlint/configuration_schema.json", - "plugins": ["typescript"], - "categories": { - "correctness": "off" - }, - "options": { - "typeAware": true, - "typeCheck": true - }, - "env": { - "builtin": true - }, - "ignorePatterns": [ - "node_modules", - "dist", - "build", - ".content-collections", - ".tanstack-start", - ".netlify", - "public", - "convex/.temp", - ".claude" - ], - "rules": { - "no-array-constructor": "error", - "no-unused-expressions": "error", - "no-unused-vars": "error", - "typescript/ban-ts-comment": "error", - "typescript/no-duplicate-enum-values": "error", - "typescript/no-empty-object-type": "error", - "typescript/no-explicit-any": "error", - "typescript/no-extra-non-null-assertion": "error", - "typescript/no-misused-new": "error", - "typescript/no-namespace": "error", - "typescript/no-non-null-asserted-optional-chain": "error", - "typescript/no-require-imports": "error", - "typescript/no-this-alias": "error", - "typescript/no-unnecessary-type-constraint": "error", - "typescript/no-unsafe-declaration-merging": "error", - "typescript/no-unsafe-function-type": "error", - "typescript/no-wrapper-object-types": "error", - "typescript/prefer-as-const": "error", - "typescript/prefer-namespace-keyword": "error", - "typescript/triple-slash-reference": "error" - }, - "overrides": [ - { - "files": ["**/*.{js,jsx}"], - "rules": { - "constructor-super": "error", - "for-direction": "error", - "no-async-promise-executor": "error", - "no-case-declarations": "error", - "no-class-assign": "error", - "no-compare-neg-zero": "error", - "no-cond-assign": "error", - "no-const-assign": "error", - "no-constant-binary-expression": "error", - "no-constant-condition": "error", - "no-control-regex": "error", - "no-debugger": "error", - "no-delete-var": "error", - "no-dupe-class-members": "error", - "no-dupe-else-if": "error", - "no-dupe-keys": "error", - "no-duplicate-case": "error", - "no-empty": "error", - "no-empty-character-class": "error", - "no-empty-pattern": "error", - "no-empty-static-block": "error", - "no-ex-assign": "error", - "no-extra-boolean-cast": "error", - "no-fallthrough": "error", - "no-func-assign": "error", - "no-global-assign": "error", - "no-import-assign": "error", - "no-invalid-regexp": "error", - "no-irregular-whitespace": "error", - "no-loss-of-precision": "error", - "no-misleading-character-class": "error", - "no-new-native-nonconstructor": "error", - "no-nonoctal-decimal-escape": "error", - "no-obj-calls": "error", - "no-prototype-builtins": "error", - "no-redeclare": "error", - "no-regex-spaces": "error", - "no-self-assign": "error", - "no-setter-return": "error", - "no-shadow-restricted-names": "error", - "no-sparse-arrays": "error", - "no-this-before-super": "error", - "no-unexpected-multiline": "error", - "no-unsafe-finally": "error", - "no-unsafe-negation": "error", - "no-unsafe-optional-chaining": "error", - "no-unused-labels": "error", - "no-unused-private-class-members": "error", - "no-useless-backreference": "error", - "no-useless-catch": "error", - "no-useless-escape": "error", - "no-with": "error", - "require-yield": "error", - "use-isnan": "error", - "valid-typeof": "error" - } - }, - { - "files": ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"], - "rules": { - "constructor-super": "off", - "no-class-assign": "off", - "no-const-assign": "off", - "no-dupe-class-members": "off", - "no-dupe-keys": "off", - "no-func-assign": "off", - "no-import-assign": "off", - "no-new-native-nonconstructor": "off", - "no-obj-calls": "off", - "no-redeclare": "off", - "no-setter-return": "off", - "no-this-before-super": "off", - "no-unsafe-negation": "off", - "no-var": "error", - "no-with": "off", - "prefer-const": "error", - "prefer-rest-params": "error", - "prefer-spread": "error" - } - }, - { - "files": ["**/*.{ts,tsx}"], - "rules": { - "no-redeclare": "error", - "no-shadow": "off", - "no-unused-vars": [ - "warn", - { - "argsIgnorePattern": "(^_)|(^__+$)|(^e$)|(^error$)", - "varsIgnorePattern": "(^_)|(^__+$)|(^e$)|(^error$)", - "caughtErrorsIgnorePattern": "(^_)|(^__+$)|(^e$)|(^error$)" - } - ], - "typescript/no-explicit-any": "off" - } - }, - { - "files": ["**/*.{ts,tsx,js,jsx}"], - "rules": { - "jsx-a11y/alt-text": "error", - "jsx-a11y/anchor-ambiguous-text": "off", - "jsx-a11y/anchor-has-content": "error", - "jsx-a11y/anchor-is-valid": "error", - "jsx-a11y/aria-activedescendant-has-tabindex": "error", - "jsx-a11y/aria-props": "error", - "jsx-a11y/aria-proptypes": "error", - "jsx-a11y/aria-role": "error", - "jsx-a11y/aria-unsupported-elements": "error", - "jsx-a11y/autocomplete-valid": "error", - "jsx-a11y/click-events-have-key-events": "error", - "jsx-a11y/heading-has-content": "error", - "jsx-a11y/html-has-lang": "error", - "jsx-a11y/iframe-has-title": "error", - "jsx-a11y/img-redundant-alt": "error", - "jsx-a11y/label-has-associated-control": "error", - "jsx-a11y/media-has-caption": "error", - "jsx-a11y/mouse-events-have-key-events": "error", - "jsx-a11y/no-access-key": "error", - "jsx-a11y/no-autofocus": "error", - "jsx-a11y/no-distracting-elements": "error", - "jsx-a11y/no-noninteractive-tabindex": [ - "error", - { - "tags": [], - "roles": ["tabpanel"], - "allowExpressionValues": true - } - ], - "jsx-a11y/no-redundant-roles": "error", - "jsx-a11y/no-static-element-interactions": [ - "error", - { - "allowExpressionValues": true, - "handlers": [ - "onClick", - "onMouseDown", - "onMouseUp", - "onKeyPress", - "onKeyDown", - "onKeyUp" - ] - } - ], - "jsx-a11y/role-has-required-aria-props": "error", - "jsx-a11y/role-supports-aria-props": "error", - "jsx-a11y/scope": "error", - "jsx-a11y/tabindex-no-positive": "error", - "react/rules-of-hooks": "error", - "react/exhaustive-deps": "warn" - }, - "plugins": ["react", "jsx-a11y"] - } - ] -} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..b40caf3a5 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +**/api +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts +convex/_generated +convex/README.md \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..fd496a820 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "semi": false +} diff --git a/AGENTS.md b/AGENTS.md deleted file mode 120000 index 49055565e..000000000 --- a/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -.agents/index.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 120000 index 49055565e..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -.agents/index.md \ No newline at end of file diff --git a/README.md b/README.md index f168f491e..90cba4aac 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,15 @@ -
+# Welcome to TanStack.com! -# TanStack.com +This site is built with TanStack Router! -The home of the TanStack ecosystem. Built with [TanStack Router](https://tanstack.com/router) and deployed on [Cloudflare Workers](https://workers.cloudflare.com/). +- [TanStack Router Docs](https://tanstack.com/router) -Follow @TanStack +It's deployed automagically with Netlify! -### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/) - -
+- [Netlify](https://netlify.com/) ## Development -### Quick Start - From your terminal: ```sh @@ -23,64 +19,54 @@ pnpm dev This starts your app in development mode, rebuilding assets on file changes. -### Local Setup +## Editing and previewing the docs of TanStack projects locally + +The documentations for all TanStack projects except for `React Charts` are hosted on [https://tanstack.com](https://tanstack.com), powered by this TanStack Router app. +In production, the markdown doc pages are fetched from the GitHub repos of the projects, but in development they are read from the local file system. -The documentation for all TanStack projects (except `React Charts`) is hosted on [tanstack.com](https://tanstack.com). In production, doc pages are fetched from GitHub. In development, they're read from your local file system. +Follow these steps if you want to edit the doc pages of a project (in these steps we'll assume it's [`TanStack/form`](https://github.com/tanstack/form)) and preview them locally : -Create a `tanstack` parent directory and clone this repo alongside the projects: +1. Create a new directory called `tanstack`. ```sh -mkdir tanstack && cd tanstack -git clone git@github.com:TanStack/tanstack.com.git -git clone git@github.com:TanStack/query.git -git clone git@github.com:TanStack/router.git -git clone git@github.com:TanStack/table.git +mkdir tanstack ``` -Your directory structure should look like this: +2. Enter the directory and clone this repo and the repo of the project there. +```sh +cd tanstack +git clone git@github.com:TanStack/tanstack.com.git +git clone git@github.com:TanStack/form.git ``` -tanstack/ - ├── tanstack.com/ - ├── query/ - ├── router/ - └── table/ -``` - -> [!WARNING] -> Directory names must match repo names exactly (e.g., `query` not `tanstack-query`). The app finds docs by looking for sibling directories by name. - -### Editing Docs - -To edit docs for a project, make changes in its `docs/` folder (e.g., `../form/docs/`) and visit http://localhost:3000/form/latest/docs/overview to preview. > [!NOTE] -> Updated pages need to be manually reloaded in the browser. +> Your `tanstack` directory should look like this: +> +> ``` +> tanstack/ +> | +> +-- form/ +> | +> +-- tanstack.com/ +> ``` > [!WARNING] -> Update the project's `docs/config.json` if you add a new doc page! +> Make sure the name of the directory in your local file system matches the name of the project's repo. For example, `tanstack/form` must be cloned into `form` (this is the default) instead of `some-other-name`, because that way, the doc pages won't be found. -## Get Involved +3. Enter the `tanstack/tanstack.com` directory, install the dependencies and run the app in dev mode: -- We welcome issues and pull requests! -- Participate in [GitHub Discussions](https://github.com/TanStack/tanstack.com/discussions) -- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ) - -## Explore the TanStack Ecosystem +```sh +cd tanstack.com +pnpm i +# The app will run on https://localhost:3000 by default +pnpm dev +``` -- TanStack Config – Tooling for JS/TS packages -- TanStack DB – Reactive sync client store -- TanStack DevTools – Unified devtools panel -- TanStack Form – Type‑safe form state -- TanStack Pacer – Debouncing, throttling, batching -- TanStack Query – Async state & caching -- TanStack Ranger – Range & slider primitives -- TanStack Router – Type‑safe routing, caching & URL state -- TanStack Start – Full‑stack SSR & streaming -- TanStack Store – Reactive data store -- TanStack Table – Headless datagrids -- TanStack Virtual – Virtualized rendering +4. Now you can visit http://localhost:3000/form/latest/docs/overview in the browser and see the changes you make in `tanstack/form/docs`. -… and more at TanStack.com » +> [!NOTE] +> The updated pages need to be manually reloaded in the browser. - +> [!WARNING] +> You will need to update the `docs/config.json` file (in the project's repo) if you add a new doc page! diff --git a/app.config.ts b/app.config.ts new file mode 100644 index 000000000..8d82995ce --- /dev/null +++ b/app.config.ts @@ -0,0 +1,52 @@ +import { sentryVitePlugin } from '@sentry/vite-plugin' +import { defineConfig } from '@tanstack/start/config' +import tsConfigPaths from 'vite-tsconfig-paths' + +export default defineConfig({ + server: { + preset: 'netlify', + }, + vite: { + plugins: [ + tsConfigPaths(), + (() => { + const replacements = [ + // replace `throw Error(p(418))` with `console.error(p(418))` + ['throw Error(p(418))', 'console.error(p(418))'], + // replace `throw new Error('Hydration failed` with `console.error('Hydration failed')` + [ + `throw new Error('Hydration failed`, + `console.error('Hydration failed`, + ], + ] as const + + return { + name: 'tanner-test', + enforce: 'post', + transform(code, id) { + replacements.forEach(([search, replacement]) => { + if (code.includes(search)) { + code = code.replaceAll(search, replacement) + } + }) + + return code + }, + } + })(), + ], + }, + routers: { + client: { + vite: { + plugins: [ + sentryVitePlugin({ + authToken: process.env.SENTRY_AUTH_TOKEN, + org: 'tanstack', + project: 'tanstack-com', + }), + ], + }, + }, + }, +}) diff --git a/app/auth/auth.ts b/app/auth/auth.ts new file mode 100644 index 000000000..7cf126840 --- /dev/null +++ b/app/auth/auth.ts @@ -0,0 +1,76 @@ +import { createCookie } from '@remix-run/node' +import { redirect } from '@tanstack/react-router' + +let secret = process.env.COOKIE_SECRET || 'default' +if (secret === 'default') { + console.warn( + '🚨 No COOKIE_SECRET environment variable set, using default. The app is insecure in production.' + ) + secret = 'default-secret' +} + +let cookie = createCookie('auth', { + secrets: [secret], + // 30 days + maxAge: 30 * 24 * 60 * 60, + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', +}) + +export async function getAuthFromRequest( + request: Request +): Promise { + const c = request.headers.get('Cookie') + let userId = await cookie.parse(c) + return userId ?? null +} + +export async function setAuthOnResponse( + response: Response, + userId: string +): Promise { + let header = await cookie.serialize(userId) + response.headers.append('Set-Cookie', header) + return response +} + +export async function requireAuthCookie(request: Request) { + let userId = await getAuthFromRequest(request) + if (!userId) { + throw redirect({ + to: '/login', + headers: { + 'Set-Cookie': await cookie.serialize('', { + maxAge: 0, + }), + }, + }) + } + return userId +} + +export async function redirectIfLoggedInLoader({ + request, +}: { + request: Request +}) { + let userId = await getAuthFromRequest(request) + if (userId) { + throw redirect({ + to: '/', + }) + } + return null +} + +export async function redirectWithClearedCookie() { + return redirect({ + to: '/', + headers: { + 'Set-Cookie': await cookie.serialize(null, { + expires: new Date(0), + }), + }, + }) +} diff --git a/src/blog/ag-grid-partnership.md b/app/blog/ag-grid-partnership.md similarity index 89% rename from src/blog/ag-grid-partnership.md rename to app/blog/ag-grid-partnership.md index 8dcf3e1d6..11ae65154 100644 --- a/src/blog/ag-grid-partnership.md +++ b/app/blog/ag-grid-partnership.md @@ -1,8 +1,6 @@ --- title: TanStack Table + Ag-Grid Partnership -published: 2022-06-17 -excerpt: AG Grid is now the official TanStack Table open-source partner. Together we'll educate the ecosystem about the differences between the two libraries and when to choose which. -library: table +published: 6/17/2022 authors: - Tanner Linsley - Niall Crosby diff --git a/src/blog/announcing-tanstack-form-v1.md b/app/blog/announcing-tanstack-form-v1.md similarity index 96% rename from src/blog/announcing-tanstack-form-v1.md rename to app/blog/announcing-tanstack-form-v1.md index d26fcd08a..406e0c34c 100644 --- a/src/blog/announcing-tanstack-form-v1.md +++ b/app/blog/announcing-tanstack-form-v1.md @@ -1,8 +1,6 @@ --- title: Announcing TanStack Form v1 -published: 2025-03-03 -excerpt: The first stable version of TanStack Form is live and ready for production. We support five frameworks at launch — React, Vue, Angular, Solid, and Lit. -library: form +published: 03/03/2025 authors: - Corbin Crutchley --- @@ -77,8 +75,8 @@ We even support type-checking what errors are returned in ``: children={(field) => ( <> - // TypeScript will correctly tell you that `errorMap.onChange` // is an - object, not a string + // TypeScript will correctly tell you that `errorMap.onChange` // is an object, + not a string

{field.state.meta.errorMap.onChange}

)} @@ -156,7 +154,7 @@ const serverValidate = createServerValidate({ export const getFormDataFromServer = createServerFn({ method: 'GET' }).handler( async () => { return getFormData() - }, + } ) ``` diff --git a/src/blog/announcing-tanstack-query-v4.md b/app/blog/announcing-tanstack-query-v4.md similarity index 93% rename from src/blog/announcing-tanstack-query-v4.md rename to app/blog/announcing-tanstack-query-v4.md index 2094f1ad3..d21152a72 100644 --- a/src/blog/announcing-tanstack-query-v4.md +++ b/app/blog/announcing-tanstack-query-v4.md @@ -1,8 +1,6 @@ --- title: Announcing TanStack Query v4 -published: 2022-07-14 -excerpt: The next version of TanStack Query is here. The rebranding and monorepo restructuring finally allows us to bring the joy of react-query to other frameworks like Vue, Svelte, and Solid. -library: query +published: 7/14/2022 authors: - Dominik Dorfmeister --- diff --git a/src/blog/announcing-tanstack-query-v5.md b/app/blog/announcing-tanstack-query-v5.md similarity index 96% rename from src/blog/announcing-tanstack-query-v5.md rename to app/blog/announcing-tanstack-query-v5.md index b061bdea3..455d97a95 100644 --- a/src/blog/announcing-tanstack-query-v5.md +++ b/app/blog/announcing-tanstack-query-v5.md @@ -1,8 +1,6 @@ --- title: Announcing TanStack Query v5 -published: 2023-10-17 -excerpt: After 91 alpha releases, 35 betas, and 16 release candidates, TanStack Query v5.0.0 is finally here — smaller, better, and more intuitive than ever. -library: query +published: 10/17/2023 authors: - Dominik Dorfmeister --- diff --git a/src/blog/netlify-partnership.md b/app/blog/netlify-partnership.md similarity index 81% rename from src/blog/netlify-partnership.md rename to app/blog/netlify-partnership.md index 02e04020f..6a39992b5 100644 --- a/src/blog/netlify-partnership.md +++ b/app/blog/netlify-partnership.md @@ -1,7 +1,6 @@ --- title: TanStack + Netlify Partnership -published: 2025-03-18 -excerpt: Netlify is now the official deployment partner for TanStack Start. Their focus on speed, simplicity, and flexibility aligns perfectly with our vision for full-stack development. +published: 3/18/2025 authors: - Tanner Linsley --- @@ -19,7 +18,7 @@ Netlify has earned its reputation as the ultimate deployment platform for modern ## Why Netlify? -Netlify is more than just a deployment provider. They’ve worked closely with us to ensure that deploying TanStack Start applications is not just fast, but optimized for the best possible developer experience. Whether you’re building interactive UIs, data-heavy dashboards, real-time tools, or AI-powered applications, Netlify’s platform makes the process seamless. +Netlify is more than just a deployment provider—they’ve worked closely with us to ensure that deploying TanStack Start applications is not just fast, but optimized for the best possible developer experience. Whether you’re building interactive UIs, data-heavy dashboards, real-time tools, or AI-powered applications, Netlify’s platform makes the process seamless. As part of this partnership, Netlify has also launched a **full-stack AI chatbot starter template** that showcases TanStack Start’s powerful data management capabilities alongside Netlify Functions. This template provides: diff --git a/src/blog/tanstack-router-typescript-performance.md b/app/blog/tanstack-router-typescript-performance.md similarity index 97% rename from src/blog/tanstack-router-typescript-performance.md rename to app/blog/tanstack-router-typescript-performance.md index 9a09550e8..3b62d6927 100644 --- a/src/blog/tanstack-router-typescript-performance.md +++ b/app/blog/tanstack-router-typescript-performance.md @@ -1,8 +1,6 @@ --- title: A milestone for TypeScript Performance in TanStack Router -published: 2024-09-17 -excerpt: TanStack Router pushes the boundaries on type-safe routing, but large route trees can slow down the editor. Here's how we achieved a major milestone in TypeScript performance. -library: router +published: 09/17/2024 authors: - Christopher Horobin --- @@ -81,11 +79,8 @@ export type ParseRoute = TRouteTree extends { ? unknown extends TChildren ? TAcc : TChildren extends ReadonlyArray - ? ParseRoute - : ParseRoute< - TChildren[keyof TChildren], - TAcc | TChildren[keyof TChildren] - > + ? ParseRoute + : ParseRoute : TAcc ``` diff --git a/src/blog/why-tanstack-start-and-router.md b/app/blog/why-tanstack-start-and-router.md similarity index 55% rename from src/blog/why-tanstack-start-and-router.md rename to app/blog/why-tanstack-start-and-router.md index 4e0168379..ce7465135 100644 --- a/src/blog/why-tanstack-start-and-router.md +++ b/app/blog/why-tanstack-start-and-router.md @@ -1,8 +1,6 @@ --- title: Why choose TanStack Start and Router? -published: 2024-12-03 -excerpt: The frameworks we choose can make or break our developer experience. Here's why TanStack Start and Router offer a compelling alternative for building modern web applications. -library: start, router +published: 12/03/2024 authors: - Tanner Linsley --- @@ -11,65 +9,65 @@ authors: Building modern web applications is no small feat. The frameworks and tools we choose can make or break not only our developer experience but also the success of the applications we build. While there are many great frameworks out there, I believe **TanStack Router** and **TanStack Start** stand apart for their ability to solve the challenges developers face today and their readiness for what’s coming tomorrow. -These aren’t just another set of tools. They represent a commitment to building better apps with less friction and more joy. Here’s why I think you’ll love working with them as much as I do. +These aren’t just another set of tools—they represent a commitment to building better apps with less friction and more joy. Here’s why I think you’ll love working with them as much as I do. ### Type Safety You Can Rely On -Type safety isn’t just a buzzword. it’s a foundational tool for creating robust, maintainable applications. TanStack Router goes beyond the basics to offer **contextual type safety**, where types flow seamlessly through every part of your app. Route definitions, parameters, navigation, and even state management all work together with fully inferred types. +Type safety isn’t just a buzzword—it’s a foundational tool for creating robust, maintainable applications. TanStack Router goes beyond the basics to offer **contextual type safety**, where types flow seamlessly through every part of your app. Route definitions, parameters, navigation, and even state management all work together with fully inferred types. What does this mean for you? It means no more guessing if you’ve defined a parameter correctly, no more debugging mismatched types, and no need for extra plugins or AST transformations to fill in the gaps. TanStack Router works with TypeScript’s natural architecture, making the experience smooth, predictable, and delightful. -This level of type safety doesn’t just save time. It builds confidence. And it’s something I believe other frameworks will spend years trying to catch up to. +This level of type safety doesn’t just save time—it builds confidence. And it’s something I believe other frameworks will spend years trying to catch up to. ### Unlock the Power of URL State Management -One of the most overlooked but powerful tools in web development is the URL. It’s the original state management system: fast, shareable, and intuitive. Yet, many frameworks treat the URL as an afterthought, offering only basic utilities for reading and writing state. +One of the most overlooked but powerful tools in web development is the URL. It’s the original state management system—fast, shareable, and intuitive. Yet, many frameworks treat the URL as an afterthought, offering only basic utilities for reading and writing state. -TanStack Router flips that script. Managing state in the URL isn’t just supported. it’s encouraged. With intuitive APIs, you can validate, read, and update search parameters with type safety and runtime validation baked in. Want to create a deeply nested, dynamic filter system or synchronize your app’s state with the URL? It’s effortless. +TanStack Router flips that script. Managing state in the URL isn’t just supported—it’s encouraged. With intuitive APIs, you can validate, read, and update search parameters with type safety and runtime validation baked in. Want to create a deeply nested, dynamic filter system or synchronize your app’s state with the URL? It’s effortless. -But this isn’t just about developer convenience. it’s about creating better user experiences. When your app state lives in the URL, users can share it, bookmark it, and pick up right where they left off. TanStack Router makes that as easy as it should be. +But this isn’t just about developer convenience—it’s about creating better user experiences. When your app state lives in the URL, users can share it, bookmark it, and pick up right where they left off. TanStack Router makes that as easy as it should be. ### Familiar Patterns, More Flexibility If you’ve worked with Remix or Next.js, you’ll find plenty of familiar concepts in TanStack Start. But familiarity doesn’t mean compromise. We’ve taken some of the best ideas from those frameworks and pushed them further, stripping away the constraints and introducing more flexibility. -For example, routing patterns and server function integrations will feel natural if you’re coming from a server-first framework like Remix, but they’re designed to work just as well for traditional client-side SPAs. You don’t have to pick a side. you get the best of both worlds, with fewer trade-offs. +For example, routing patterns and server function integrations will feel natural if you’re coming from a server-first framework like Remix, but they’re designed to work just as well for traditional client-side SPAs. You don’t have to pick a side—you get the best of both worlds, with fewer trade-offs. ### Built for the Future (and Already Embracing It) -The web is changing fast. With React Server Components (RSCs) on the horizon, React 19 introducing new patterns, and streaming becoming the standard for data delivery, frameworks need to do more than just keep up. They need to lead. +The web is changing fast. With React Server Components (RSCs) on the horizon, React 19 introducing new patterns, and streaming becoming the standard for data delivery, frameworks need to do more than just keep up—they need to lead. -TanStack Start is ready for what’s next. RSCs are treated as another server-side state, with powerful primitives for caching, invalidating, and composing them into your application. Streaming isn’t an afterthought. it’s baked into the core of how TanStack works, giving you tools to incrementally send data and HTML to the client without extra complexity. +TanStack Start is ready for what’s next. RSCs are treated as another server-side state, with powerful primitives for caching, invalidating, and composing them into your application. Streaming isn’t an afterthought—it’s baked into the core of how TanStack works, giving you tools to incrementally send data and HTML to the client without extra complexity. But we’re not just about future-proofing. TanStack Start also makes these advanced capabilities approachable and usable today. You don’t need to wait for the “next big thing” to start building apps that feel like the future. ### SPAs Aren’t Dead (We’re Just Making Them Better) -There’s a lot of talk about server-first architectures these days, and while they’re exciting, they’re not the whole story. Single Page Applications (SPAs) are still an incredible way to build fast, interactive apps. Especially when done right. +There’s a lot of talk about server-first architectures these days, and while they’re exciting, they’re not the whole story. Single Page Applications (SPAs) are still an incredible way to build fast, interactive apps—especially when done right. -TanStack Start doesn’t just keep SPAs viable. It makes them better. With simplified patterns, powerful state management, and deep integrations, you can build SPAs that are more performant, easier to maintain, and a joy to use. Whether you’re working server-first, client-first, or somewhere in between, TanStack gives you the tools to build the app you want. +TanStack Start doesn’t just keep SPAs viable—it makes them better. With simplified patterns, powerful state management, and deep integrations, you can build SPAs that are more performant, easier to maintain, and a joy to use. Whether you’re working server-first, client-first, or somewhere in between, TanStack gives you the tools to build the app you want. ### Data Integration Like No Other If you’ve used **TanStack Query**, you already know how much it simplifies data-fetching. But the integration between TanStack Query and TanStack Router is where the magic really happens. Prefetching data, caching results, and streaming updates are all seamless, intuitive, and built to scale. -For example, you can prefetch data in a route loader, stream it down to the client, and hydrate it on demand. All with a single API. Whether you’re managing a simple blog or a complex dashboard, you’ll find yourself spending less time wiring up data and more time building features. +For example, you can prefetch data in a route loader, stream it down to the client, and hydrate it on demand—all with a single API. Whether you’re managing a simple blog or a complex dashboard, you’ll find yourself spending less time wiring up data and more time building features. -This isn’t just an integration. it’s a partnership between routing and data-fetching that makes everything else feel clunky by comparison. +This isn’t just an integration—it’s a partnership between routing and data-fetching that makes everything else feel clunky by comparison. ### Routing That Scales -Routing isn’t just a utility. it’s the backbone of every application. And yet, most routers struggle when things get complex. That’s where TanStack Router shines. It’s built to handle everything from a handful of simple routes to thousands of deeply nested ones without breaking a sweat. +Routing isn’t just a utility—it’s the backbone of every application. And yet, most routers struggle when things get complex. That’s where TanStack Router shines. It’s built to handle everything from a handful of simple routes to thousands of deeply nested ones without breaking a sweat. Features like type-safe navigation, hierarchical route contexts, and advanced state synchronization make it easy to build apps that scale in both size and complexity. And because TanStack Router works natively with TypeScript, you get all the benefits of type safety without sacrificing performance or flexibility. ### Always Innovating -What excites me most about TanStack is that we’re just getting started. From isomorphic server functions to powerful caching primitives and streamlined React Server Component support, we’re constantly pushing the boundaries of what’s possible. Our goal isn’t just to build great tools. it’s to build tools that help _you_ build better apps. +What excites me most about TanStack is that we’re just getting started. From isomorphic server functions to powerful caching primitives and streamlined React Server Component support, we’re constantly pushing the boundaries of what’s possible. Our goal isn’t just to build great tools—it’s to build tools that help _you_ build better apps. ### Settle for “Good Enough”? -Other frameworks have their strengths, but if you’re looking for tools that are innovative, flexible, and deeply integrated, TanStack Router and Start are in a league of their own. They’re not just solving today’s problems. They’re helping you build apps that are ready for tomorrow. +Other frameworks have their strengths, but if you’re looking for tools that are innovative, flexible, and deeply integrated, TanStack Router and Start are in a league of their own. They’re not just solving today’s problems—they’re helping you build apps that are ready for tomorrow. So why wait? Explore [TanStack Router](https://tanstack.com/router) and [TanStack Start](https://tanstack.com/start) today, and see how much better app development can be. diff --git a/src/blog/why-tanstack-start-is-ditching-adapters.md b/app/blog/why-tanstack-start-is-ditching-adapters.md similarity index 96% rename from src/blog/why-tanstack-start-is-ditching-adapters.md rename to app/blog/why-tanstack-start-is-ditching-adapters.md index 0621a2fde..a2a7bf218 100644 --- a/src/blog/why-tanstack-start-is-ditching-adapters.md +++ b/app/blog/why-tanstack-start-is-ditching-adapters.md @@ -1,8 +1,6 @@ --- title: Why TanStack Start is Ditching Adapters -published: 2024-11-22 -excerpt: Every cloud environment has its own quirky incantations to get things working. We're dropping custom adapters in TanStack Start and building on Nitro instead — here's why. -library: start +published: 11/22/2024 authors: - Tanner Linsley --- @@ -44,7 +42,7 @@ By using Nitro, all of TanStack Start’s adapter problems were gone. I never ev In fact, to deploy to Vercel, it was even easier than I had initially planned: just pass a `vercel` target to our `defineConfig`’s `server.preset` option, which is passed to Nitro: ```jsx -import { defineConfig } from '@tanstack/react-start/config' +import { defineConfig } from '@tanstack/start/config' export default defineConfig({ server: { diff --git a/app/client.tsx b/app/client.tsx new file mode 100644 index 000000000..41f74b4a3 --- /dev/null +++ b/app/client.tsx @@ -0,0 +1,9 @@ +/// +import { hydrateRoot } from 'react-dom/client' +import { StartClient } from '@tanstack/start' +import { createRouter } from './router' +import './utils/sentry' + +const router = createRouter() + +hydrateRoot(document, ) diff --git a/app/components/BackgroundAnimation.tsx b/app/components/BackgroundAnimation.tsx new file mode 100644 index 000000000..29037a28f --- /dev/null +++ b/app/components/BackgroundAnimation.tsx @@ -0,0 +1,231 @@ +import * as React from 'react' +import { usePrefersReducedMotion } from '~/utils/usePrefersReducedMotion' +import { twMerge } from 'tailwind-merge' +import { useMounted } from '~/hooks/useMounted' +import { useRouterState } from '@tanstack/react-router' + +export function BackgroundAnimation() { + const canvasRef = React.useRef(null) + const prefersReducedMotion = usePrefersReducedMotion() + const mounted = useMounted() + const isHomePage = useRouterState({ + select: (s) => s.location.pathname === '/', + }) + + React.useEffect(() => { + if (prefersReducedMotion !== false) { + return + } + + const canvas = canvasRef.current + + let morphDuration = 2000 + const waitDuration = 1000 * 60 * 2 + + const easingFn = cubicBezier(0.645, 0.045, 0.355, 1.0) + + if (canvas) { + const ctx = canvas.getContext('2d')! + + let rafId: ReturnType | null = null + let timeout: ReturnType | null = null + let startTime = performance.now() + + function createBlobs() { + return shuffle([ + { + color: { h: 10, s: 100, l: 50 }, + }, + { + color: { h: 40, s: 100, l: 50 }, + }, + { + color: { h: 150, s: 100, l: 50 }, + }, + { + color: { h: 200, s: 100, l: 50 }, + }, + ]).map((blob) => ({ + ...blob, + x: Math.random() * canvas!.width, + y: Math.random() * canvas!.height, + r: Math.random() * 500 + 700, + colorH: blob.color.h, + colorS: blob.color.s, + colorL: blob.color.l, + })) + } + + function shuffle(array: T[]) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[array[i], array[j]] = [array[j], array[i]] + } + return array + } + + let startBlobs = createBlobs() + let currentBlobs = startBlobs + let targetBlobs: ReturnType = [] + + function resizeHandler() { + // Create an offscreen canvas and copy the current content + const offscreen = document.createElement('canvas') + offscreen.width = canvas!.width + offscreen.height = canvas!.height + offscreen.getContext('2d')!.drawImage(canvas!, 0, 0) + + // Resize the main canvas + canvas!.width = window.innerWidth + canvas!.height = window.innerHeight + + // Stretch and redraw the saved content to fill the new size + ctx.drawImage(offscreen, 0, 0, canvas!.width, canvas!.height) + } + + function start() { + if (timeout) { + clearTimeout(timeout) + } + if (rafId) { + cancelAnimationFrame(rafId) + } + + startBlobs = JSON.parse(JSON.stringify(currentBlobs)) + targetBlobs = createBlobs() + startTime = performance.now() + animate() + } + + function animate() { + ctx.clearRect(0, 0, canvas!.width, canvas!.height) + + const time = performance.now() - startTime + const progress = time / morphDuration + const easedProgress = easingFn(progress) + + // Draw the blobs + startBlobs.forEach((startBlob, i) => { + const targetBlob = targetBlobs[i] + + currentBlobs[i].x = interpolate( + startBlob.x, + targetBlob.x, + easedProgress + ) + currentBlobs[i].y = interpolate( + startBlob.y, + targetBlob.y, + easedProgress + ) + + const gradient = ctx.createRadialGradient( + currentBlobs[i].x, + currentBlobs[i].y, + 0, + currentBlobs[i].x, + currentBlobs[i].y, + currentBlobs[i].r + ) + + currentBlobs[i].colorH = interpolate( + startBlob.colorH, + targetBlob.colorH, + easedProgress + ) + currentBlobs[i].colorS = interpolate( + startBlob.colorS, + targetBlob.colorS, + easedProgress + ) + currentBlobs[i].colorL = interpolate( + startBlob.colorL, + targetBlob.colorL, + easedProgress + ) + + gradient.addColorStop( + 0, + `hsla(${currentBlobs[i].colorH}, ${currentBlobs[i].colorS}%, ${currentBlobs[i].colorL}%, 1)` + ) + gradient.addColorStop( + 1, + `hsla(${currentBlobs[i].colorH}, ${currentBlobs[i].colorS}%, ${currentBlobs[i].colorL}%, 0)` + ) + + ctx.fillStyle = gradient + ctx.beginPath() + ctx.arc( + currentBlobs[i].x, + currentBlobs[i].y, + currentBlobs[i].r, + 0, + Math.PI * 2 + ) + ctx.fill() + }) + + if (progress < 1) { + rafId = requestAnimationFrame(animate) + } else { + timeout = setTimeout(() => { + morphDuration = 4000 + start() + }, waitDuration) + } + } + + resizeHandler() + start() + window.addEventListener('resize', resizeHandler) + + return () => { + if (rafId) { + cancelAnimationFrame(rafId) + } + if (timeout) { + clearTimeout(timeout) + } + window.removeEventListener('resize', resizeHandler) + } + } + }, [prefersReducedMotion]) + + return ( +
+ +
+ ) +} + +function cubicBezier(p1x: number, p1y: number, p2x: number, p2y: number) { + return function (t: number) { + const cx = 3 * p1x + const bx = 3 * (p2x - p1x) - cx + const ax = 1 - cx - bx + + const cy = 3 * p1y + const by = 3 * (p2y - p1y) - cy + const ay = 1 - cy - by + + const x = ((ax * t + bx) * t + cx) * t + const y = ((ay * t + by) * t + cy) * t + + return y + } +} + +function interpolate(start: number, end: number, progress: number) { + return start + (end - start) * progress +} diff --git a/app/components/BytesForm.tsx b/app/components/BytesForm.tsx new file mode 100644 index 000000000..ae12b2622 --- /dev/null +++ b/app/components/BytesForm.tsx @@ -0,0 +1,43 @@ +import useBytesSubmit from '~/components/useBytesSubmit' +import bytesImage from '~/images/bytes.svg' + +export default function BytesForm() { + const { state, handleSubmit, error } = useBytesSubmit() + if (state === 'submitted') { + return ( +

Success! Please, check your email to confirm your subscription.

+ ) + } + return ( +
+
+
+
+ Bytes +
+ +
+ +
+

+ No spam. Unsubscribe at any time. +

+ {error &&

{error}

} +
+ ) +} diff --git a/app/components/Carbon.tsx b/app/components/Carbon.tsx new file mode 100644 index 000000000..25f99957c --- /dev/null +++ b/app/components/Carbon.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' + +export function Carbon() { + const ref = React.useRef(null!) + + React.useEffect(() => { + ref.current.innerHTML = '' + const s = document.createElement('script') + s.id = '_carbonads_js' + s.src = `//cdn.carbonads.com/carbon.js?serve=CE7DEKQI&placement=react-tannerlinsleycom` + ref.current.appendChild(s) + }, []) + + return
+} diff --git a/src/components/CodeExplorer.tsx b/app/components/CodeExplorer.tsx similarity index 74% rename from src/components/CodeExplorer.tsx rename to app/components/CodeExplorer.tsx index 773b37b5a..c97fbf7c9 100644 --- a/src/components/CodeExplorer.tsx +++ b/app/components/CodeExplorer.tsx @@ -1,12 +1,38 @@ import React from 'react' +import { CodeBlock } from '~/components/Markdown' import { FileExplorer } from './FileExplorer' import { InteractiveSandbox } from './InteractiveSandbox' import { CodeExplorerTopBar } from './CodeExplorerTopBar' import type { GitHubFileNode } from '~/utils/documents.server' import type { Library } from '~/libraries' -import { twMerge } from 'tailwind-merge' -import { CodeBlock } from '~/components/markdown' -import { getCodeBlockLanguageFromFilePath } from '~/components/markdown/codeBlock.shared' + +function overrideExtension(ext: string | undefined) { + if (!ext) return 'txt' + + // Override some extensions + if (['cts', 'mts'].includes(ext)) return 'ts' + if (['cjs', 'mjs'].includes(ext)) return 'js' + if (['prettierrc', 'babelrc', 'webmanifest'].includes(ext)) return 'json' + if (['env', 'example'].includes(ext)) return 'sh' + if ( + [ + 'gitignore', + 'prettierignore', + 'log', + 'gitattributes', + 'editorconfig', + 'lock', + 'opts', + 'Dockerfile', + 'dockerignore', + 'npmrc', + 'nvmrc', + ].includes(ext) + ) + return 'txt' + + return ext +} interface CodeExplorerProps { activeTab: 'code' | 'sandbox' @@ -37,7 +63,6 @@ export function CodeExplorer({ }: CodeExplorerProps) { const [isFullScreen, setIsFullScreen] = React.useState(false) const [isSidebarOpen, setIsSidebarOpen] = React.useState(true) - const currentCodeLanguage = getCodeBlockLanguageFromFilePath(currentPath) // Add escape key handler React.useEffect(() => { @@ -62,9 +87,7 @@ export function CodeExplorer({ return (
-
+
- + {currentCode} diff --git a/src/components/CodeExplorerTopBar.tsx b/app/components/CodeExplorerTopBar.tsx similarity index 71% rename from src/components/CodeExplorerTopBar.tsx rename to app/components/CodeExplorerTopBar.tsx index a76ebfe0b..b998ca205 100644 --- a/src/components/CodeExplorerTopBar.tsx +++ b/app/components/CodeExplorerTopBar.tsx @@ -1,11 +1,6 @@ import React from 'react' -import { - ArrowLeftFromLine, - ArrowRightFromLine, - Maximize, - Minimize, - TextAlignStart, -} from 'lucide-react' +import { FaExpand, FaCompress } from 'react-icons/fa' +import { CgMenuLeft } from 'react-icons/cg' interface CodeExplorerTopBarProps { activeTab: 'code' | 'sandbox' @@ -28,26 +23,16 @@ export function CodeExplorerTopBar({
{activeTab === 'code' ? ( - isSidebarOpen ? ( - - ) : ( - - ) + ) : (
- +
)}
diff --git a/app/components/CookieConsent.tsx b/app/components/CookieConsent.tsx new file mode 100644 index 000000000..86ae3b58d --- /dev/null +++ b/app/components/CookieConsent.tsx @@ -0,0 +1,268 @@ +import { Link } from '@tanstack/react-router' +import { useEffect, useState } from 'react' + +declare global { + interface Window { + dataLayer: any[] + gtag: any + } +} + +const EU_COUNTRIES = [ + 'AT', + 'BE', + 'BG', + 'CZ', + 'DE', + 'DK', + 'EE', + 'ES', + 'FI', + 'FR', + 'GB', + 'GR', + 'HR', + 'HU', + 'IE', + 'IS', + 'IT', + 'LT', + 'LU', + 'LV', + 'MT', + 'NL', + 'NO', + 'PL', + 'PT', + 'RO', + 'SE', + 'SI', + 'SK', + 'CH', +] + +export default function CookieConsent() { + const [showBanner, setShowBanner] = useState(false) + const [showSettings, setShowSettings] = useState(false) + const consentSettings = + typeof document !== 'undefined' + ? JSON.parse(localStorage.getItem('cookie_consent') || '{}') + : { analytics: false, ads: false } + + useEffect(() => { + const checkLocationAndSetConsent = async () => { + // Only check location if no consent has been set yet + if (!consentSettings.analytics && !consentSettings.ads) { + try { + const response = await fetch( + 'https://www.cloudflare.com/cdn-cgi/trace' + ) + const data = await response.text() + const country = data.match(/loc=(\w+)/)?.[1] + const isEU = country ? EU_COUNTRIES.includes(country) : false + + if (isEU) { + // Set default denied consent for EU users + const euConsent = { analytics: false, ads: false } + localStorage.setItem('cookie_consent', JSON.stringify(euConsent)) + updateGTMConsent(euConsent) + setShowBanner(true) + } else { + // For non-EU users, set default accepted consent and don't show banner + const nonEuConsent = { analytics: true, ads: true } + localStorage.setItem('cookie_consent', JSON.stringify(nonEuConsent)) + updateGTMConsent(nonEuConsent) + setShowBanner(false) + } + } catch (error) { + console.error('Error checking location:', error) + setShowBanner(true) + } + } else { + updateGTMConsent(consentSettings) + } + } + + checkLocationAndSetConsent() + }, []) + + const updateGTMConsent = (settings: { analytics: boolean; ads: boolean }) => { + window.dataLayer = window.dataLayer || [] + window.dataLayer.push({ + event: 'cookie_consent', + consent: { + analytics_storage: settings.analytics ? 'granted' : 'denied', + ad_storage: settings.ads ? 'granted' : 'denied', + ad_personalization: settings.ads ? 'granted' : 'denied', + }, + }) + + if (typeof window.gtag === 'function') { + window.gtag('consent', 'update', { + analytics_storage: settings.analytics ? 'granted' : 'denied', + ad_storage: settings.ads ? 'granted' : 'denied', + ad_personalization: settings.ads ? 'granted' : 'denied', + }) + } + + if (settings.analytics || settings.ads) { + restoreGoogleScripts() + } else { + blockGoogleScripts() + } + } + + const acceptAllCookies = () => { + const consent = { analytics: true, ads: true } + localStorage.setItem('cookie_consent', JSON.stringify(consent)) + updateGTMConsent(consent) + setShowBanner(false) + } + + const rejectAllCookies = () => { + const consent = { analytics: false, ads: false } + localStorage.setItem('cookie_consent', JSON.stringify(consent)) + updateGTMConsent(consent) + setShowBanner(false) + } + + const openSettings = () => setShowSettings(true) + const closeSettings = () => setShowSettings(false) + + const blockGoogleScripts = () => { + document.querySelectorAll('script').forEach((script) => { + if ( + script.src?.includes('googletagmanager.com') || + script.textContent?.includes('gtag(') + ) { + script.remove() + } + }) + document.cookie = + '_ga=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.google.com' + document.cookie = + '_gid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.google.com' + } + + const restoreGoogleScripts = () => { + if (!document.querySelector("script[src*='googletagmanager.com']")) { + const script = document.createElement('script') + script.src = 'https://www.googletagmanager.com/gtag/js?id=GTM-5N57KQT4' + script.async = true + document.body.appendChild(script) + } + } + + return ( + <> + {showBanner && ( +
+ + We use cookies for site functionality, analytics, and ads{' '} + + (which is a large part of how TanStack OSS remains free forever) + + . See our{' '} + + Privacy Policy + {' '} + for details. + +
+ + + +
+
+ )} + + {showSettings && ( +
+
+

Cookie Settings

+
+ + +
+ +
+
+
+
+ )} + + ) +} diff --git a/src/components/DefaultCatchBoundary.tsx b/app/components/DefaultCatchBoundary.tsx similarity index 70% rename from src/components/DefaultCatchBoundary.tsx rename to app/components/DefaultCatchBoundary.tsx index 5083603cd..1c5747f2f 100644 --- a/src/components/DefaultCatchBoundary.tsx +++ b/app/components/DefaultCatchBoundary.tsx @@ -6,11 +6,6 @@ import { useMatch, useRouter, } from '@tanstack/react-router' -import * as Sentry from '@sentry/tanstackstart-react' - -import { Button } from '~/ui' -import { reloadOnStaleAppError } from '~/utils/stale-app-reload' -import { useEffect } from 'react' // type DefaultCatchBoundaryType = { // status: number @@ -21,12 +16,6 @@ import { useEffect } from 'react' export function DefaultCatchBoundary({ error }: ErrorComponentProps) { const router = useRouter() - - useEffect(() => { - if (reloadOnStaleAppError(error)) return - Sentry.captureException(error) - }, [error]) - const isRoot = useMatch({ strict: false, select: (state) => state.id === rootRouteId, @@ -44,28 +33,32 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
- + {isRoot ? ( - + ) : ( - + )}
diff --git a/app/components/Doc.tsx b/app/components/Doc.tsx new file mode 100644 index 000000000..e93edfc4b --- /dev/null +++ b/app/components/Doc.tsx @@ -0,0 +1,150 @@ +import * as React from 'react' +import { FaEdit } from 'react-icons/fa' +import { marked } from 'marked' +import markedAlert from 'marked-alert' +import { gfmHeadingId, getHeadingList } from 'marked-gfm-heading-id' +import { DocTitle } from '~/components/DocTitle' +import { Markdown } from '~/components/Markdown' +import { Toc } from './Toc' +import { twMerge } from 'tailwind-merge' +import { TocMobile } from './TocMobile' +import { GadLeader } from './GoogleScripts' + +type DocProps = { + title: string + content: string + repo: string + branch: string + filePath: string + shouldRenderToc?: boolean + colorFrom?: string + colorTo?: string +} + +export function Doc({ + title, + content, + repo, + branch, + filePath, + shouldRenderToc = false, + colorFrom, + colorTo, +}: DocProps) { + const { markup, headings } = React.useMemo(() => { + const markup = marked.use( + { gfm: true }, + gfmHeadingId(), + markedAlert() + )(content) as string + + const headings = getHeadingList() + + return { markup, headings } + }, [content]) + + const isTocVisible = shouldRenderToc && headings && headings.length > 1 + + const markdownContainerRef = React.useRef(null) + const [activeHeadings, setActiveHeadings] = React.useState>([]) + + const headingElementRefs = React.useRef< + Record + >({}) + + React.useEffect(() => { + const callback = (headingsList: Array) => { + headingElementRefs.current = headingsList.reduce( + (map, headingElement) => { + map[headingElement.target.id] = headingElement + return map + }, + headingElementRefs.current + ) + + const visibleHeadings: Array = [] + Object.keys(headingElementRefs.current).forEach((key) => { + const headingElement = headingElementRefs.current[key] + if (headingElement.isIntersecting) { + visibleHeadings.push(headingElement) + } + }) + + if (visibleHeadings.length >= 1) { + setActiveHeadings(visibleHeadings.map((h) => h.target.id)) + } + } + + const observer = new IntersectionObserver(callback, { + rootMargin: '0px', + threshold: 0.2, + }) + + const headingElements = Array.from( + markdownContainerRef.current?.querySelectorAll( + 'h2[id], h3[id], h4[id], h5[id], h6[id]' + ) ?? [] + ) + headingElements.forEach((el) => observer.observe(el)) + + return () => observer.disconnect() + }, []) + + return ( + + {shouldRenderToc ? : null} +
+
+ + {title ? {title} : null} +
+
+
+
+ +
+
+
+ +
+
+ + {isTocVisible && ( +
+ +
+ )} +
+ + ) +} diff --git a/src/components/DocContainer.tsx b/app/components/DocContainer.tsx similarity index 63% rename from src/components/DocContainer.tsx rename to app/components/DocContainer.tsx index 6d6228fe1..10943b9f3 100644 --- a/src/components/DocContainer.tsx +++ b/app/components/DocContainer.tsx @@ -6,7 +6,13 @@ export function DocContainer({ ...props }: { children: React.ReactNode } & HTMLProps) { return ( -
+
{children}
) diff --git a/src/components/DocTitle.tsx b/app/components/DocTitle.tsx similarity index 100% rename from src/components/DocTitle.tsx rename to app/components/DocTitle.tsx diff --git a/app/components/DocsCalloutBytes.tsx b/app/components/DocsCalloutBytes.tsx new file mode 100644 index 000000000..2fdeaa4a9 --- /dev/null +++ b/app/components/DocsCalloutBytes.tsx @@ -0,0 +1,18 @@ +import BytesForm from '~/components/BytesForm' + +export function DocsCalloutBytes(props: React.HTMLProps) { + return ( +
+
+
+ Subscribe to Bytes +
+

+ Your weekly dose of JavaScript news. Delivered every Monday to over + 100,000 devs, for free. +

+
+ +
+ ) +} diff --git a/app/components/DocsCalloutQueryGG.tsx b/app/components/DocsCalloutQueryGG.tsx new file mode 100644 index 000000000..5ab6aa71c --- /dev/null +++ b/app/components/DocsCalloutQueryGG.tsx @@ -0,0 +1,41 @@ +import { LogoQueryGGSmall } from '~/components/LogoQueryGGSmall' +import { useQueryGGPPPDiscount } from '~/hooks/useQueryGGPPPDiscount' + +export function DocsCalloutQueryGG() { + const ppp = useQueryGGPPPDiscount() + + return ( + +
+
+ Want to Skip the Docs? +
+ + +
+ “If you're serious about *really* understanding React Query, there's + no better way than with query.gg” + —Tanner Linsley +
+ + {ppp && ( + <> +

+ To help make query.gg more accessible, you can enable a regional + discount of {ppp.discount * 100}% off for being in {ppp.flag}{' '} + {ppp.country}. +

+ + )} + +
+
+ ) +} diff --git a/app/components/DocsLayout.tsx b/app/components/DocsLayout.tsx new file mode 100644 index 000000000..622d6b7d5 --- /dev/null +++ b/app/components/DocsLayout.tsx @@ -0,0 +1,748 @@ +import * as React from 'react' +import { CgClose, CgMenuLeft } from 'react-icons/cg' +import { + FaArrowLeft, + FaArrowRight, + FaDiscord, + FaGithub, + FaTimes, +} from 'react-icons/fa' +import { + Link, + useMatches, + useNavigate, + useParams, +} from '@tanstack/react-router' +import { FrameworkSelect } from '~/components/FrameworkSelect' +import { useLocalStorage } from '~/utils/useLocalStorage' +import { DocsLogo } from '~/components/DocsLogo' +import { last, capitalize } from '~/utils/utils' +import type { SelectOption } from '~/components/FrameworkSelect' +import type { ConfigSchema, MenuItem } from '~/utils/config' +import { create } from 'zustand' +import { Framework, getFrameworkOptions } from '~/libraries' +import { DocsCalloutQueryGG } from '~/components/DocsCalloutQueryGG' +import { DocsCalloutBytes } from '~/components/DocsCalloutBytes' +import { twMerge } from 'tailwind-merge' +import { partners } from '~/utils/partners' +import { useThemeStore } from './ThemeToggle' +import { + GadFooter, + GadLeftRailSquare, + GadRightRailSquare, +} from './GoogleScripts' +import { SearchButton } from './SearchButton' + +// Let's use zustand to wrap the local storage logic. This way +// we'll get subscriptions for free and we can use it in other +// components if we need to. +const useLocalCurrentFramework = create<{ + currentFramework?: string + setCurrentFramework: (framework: string) => void +}>((set) => ({ + currentFramework: + typeof document !== 'undefined' + ? localStorage.getItem('framework') || undefined + : undefined, + setCurrentFramework: (framework: string) => { + localStorage.setItem('framework', framework) + set({ currentFramework: framework }) + }, +})) + +/** + * Use framework in URL path + * Otherwise use framework in localStorage if it exists for this project + * Otherwise fallback to react + */ +function useCurrentFramework(frameworks: Framework[]) { + const navigate = useNavigate() + + const { framework: paramsFramework } = useParams({ + strict: false, + }) + + const localCurrentFramework = useLocalCurrentFramework() + + let framework = (paramsFramework || + localCurrentFramework.currentFramework || + 'react') as Framework + + framework = frameworks.includes(framework) ? framework : 'react' + + const setFramework = React.useCallback( + (framework: string) => { + navigate({ + params: (prev) => ({ + ...prev, + framework, + }), + }) + localCurrentFramework.setCurrentFramework(framework) + }, + [localCurrentFramework, navigate] + ) + + React.useEffect(() => { + // Set the framework in localStorage if it doesn't exist + if (!localCurrentFramework.currentFramework) { + localCurrentFramework.setCurrentFramework(framework) + } + + // Set the framework in localStorage if it doesn't match the URL + if ( + paramsFramework && + paramsFramework !== localCurrentFramework.currentFramework + ) { + localCurrentFramework.setCurrentFramework(paramsFramework) + } + }) + + return { + framework, + setFramework, + } +} + +// Let's use zustand to wrap the local storage logic. This way +// we'll get subscriptions for free and we can use it in other +// components if we need to. +const useLocalCurrentVersion = create<{ + currentVersion?: string + setCurrentVersion: (version: string) => void +}>((set) => ({ + currentVersion: + typeof document !== 'undefined' + ? localStorage.getItem('version') || undefined + : undefined, + setCurrentVersion: (version: string) => { + localStorage.setItem('version', version) + set({ currentVersion: version }) + }, +})) + +/** + * Use framework in URL path + * Otherwise use framework in localStorage if it exists for this project + * Otherwise fallback to react + */ +function useCurrentVersion(versions: string[]) { + const navigate = useNavigate() + + const { version: paramsVersion } = useParams({ + strict: false, + }) + + const localCurrentVersion = useLocalCurrentVersion() + + let version = paramsVersion || localCurrentVersion.currentVersion || 'latest' + + version = versions.includes(version) ? version : 'latest' + + const setVersion = React.useCallback( + (version: string) => { + navigate({ + params: (prev: Record) => ({ + ...prev, + version, + }), + }) + localCurrentVersion.setCurrentVersion(version) + }, + [localCurrentVersion, navigate] + ) + + React.useEffect(() => { + // Set the version in localStorage if it doesn't exist + if (!localCurrentVersion.currentVersion) { + localCurrentVersion.setCurrentVersion(version) + } + + // Set the version in localStorage if it doesn't match the URL + if (paramsVersion && paramsVersion !== localCurrentVersion.currentVersion) { + localCurrentVersion.setCurrentVersion(paramsVersion) + } + }) + + return { + version, + setVersion, + } +} + +const useMenuConfig = ({ + config, + repo, + frameworks, +}: { + config: ConfigSchema + repo: string + frameworks: Framework[] +}): MenuItem[] => { + const currentFramework = useCurrentFramework(frameworks) + + const localMenu: MenuItem = { + label: 'Menu', + children: [ + { + label: 'Home', + to: '..', + }, + ...(frameworks.length > 1 + ? [ + { + label: 'Frameworks', + to: './framework', + }, + ] + : []), + { + label: ( +
+ GitHub +
+ ), + to: `https://github.com/${repo}`, + }, + { + label: ( +
+ Discord +
+ ), + to: 'https://tlinz.com/discord', + }, + ], + } + + return [ + localMenu, + // Merge the two menus together based on their group labels + ...config.sections.map((section): MenuItem | undefined => { + const frameworkDocs = section.frameworks?.find( + (f) => f.label === currentFramework.framework + ) + const frameworkItems = frameworkDocs?.children ?? [] + + const children = [ + ...section.children.map((d) => ({ ...d, badge: 'core' })), + ...frameworkItems.map((d) => ({ + ...d, + badge: currentFramework.framework, + })), + ] + + if (children.length === 0) { + return undefined + } + + return { + label: section.label, + children, + collapsible: section.collapsible ?? false, + defaultCollapsed: section.defaultCollapsed ?? false, + } + }), + ].filter((item) => item !== undefined) +} + +const useFrameworkConfig = ({ frameworks }: { frameworks: Framework[] }) => { + const currentFramework = useCurrentFramework(frameworks) + + const frameworkConfig = React.useMemo(() => { + return { + label: 'Framework', + selected: frameworks.includes(currentFramework.framework) + ? currentFramework.framework + : 'react', + available: getFrameworkOptions(frameworks), + onSelect: (option: { label: string; value: string }) => { + currentFramework.setFramework(option.value) + }, + } + }, [frameworks, currentFramework]) + + return frameworkConfig +} + +const useVersionConfig = ({ versions }: { versions: string[] }) => { + const currentVersion = useCurrentVersion(versions) + + const versionConfig = React.useMemo(() => { + const available = versions.reduce( + (acc: SelectOption[], version) => { + acc.push({ + label: version, + value: version, + }) + return acc + }, + [ + { + label: 'Latest', + value: 'latest', + }, + ] + ) + + return { + label: 'Version', + selected: versions.includes(currentVersion.version) + ? currentVersion.version + : 'latest', + available, + onSelect: (option: { label: string; value: string }) => { + currentVersion.setVersion(option.value) + }, + } + }, [currentVersion, versions]) + + return versionConfig +} + +type DocsLayoutProps = { + name: string + version: string + colorFrom: string + colorTo: string + textColor: string + config: ConfigSchema + frameworks: Framework[] + versions: string[] + repo: string + children: React.ReactNode +} + +export function DocsLayout({ + name, + version, + colorFrom, + colorTo, + textColor, + config, + frameworks, + versions, + repo, + children, +}: DocsLayoutProps) { + const { libraryId } = useParams({ + from: '/$libraryId/$version/docs', + }) + const { _splat } = useParams({ strict: false }) + const frameworkConfig = useFrameworkConfig({ frameworks }) + const versionConfig = useVersionConfig({ versions }) + const menuConfig = useMenuConfig({ config, frameworks, repo }) + + const matches = useMatches() + const lastMatch = last(matches) + + const isExample = matches.some((d) => d.pathname.includes('/examples/')) + + const detailsRef = React.useRef(null!) + + const flatMenu = React.useMemo( + () => menuConfig.flatMap((d) => d?.children), + [menuConfig] + ) + + const docsMatch = matches.find((d) => d.pathname.includes('/docs')) + + const relativePathname = lastMatch.pathname.replace( + docsMatch!.pathname + '/', + '' + ) + + const index = flatMenu.findIndex((d) => d?.to === relativePathname) + const prevItem = flatMenu[index - 1] + const nextItem = flatMenu[index + 1] + + const [showBytes, setShowBytes] = useLocalStorage('showBytes', true) + + const [mounted, setMounted] = React.useState(false) + + React.useEffect(() => { + setMounted(true) + }, []) + + const menuItems = menuConfig.map((group, i) => { + const WrapperComp = group.collapsible ? 'details' : 'div' + const LabelComp = group.collapsible ? 'summary' : 'div' + + const isChildActive = group.children.some((d) => d.to === _splat) + const configGroupOpenState = + typeof group.defaultCollapsed !== 'undefined' + ? !group.defaultCollapsed // defaultCollapsed is true means the group is closed + : undefined + const isOpen = isChildActive ? true : configGroupOpenState ?? false + + const detailsProps = group.collapsible ? { open: isOpen } : {} + + return ( + + + {group?.label} + +
+
    + {group?.children?.map((child, i) => { + const linkClasses = `cursor-pointer flex gap-2 items-center justify-between group px-2 py-[.1rem] rounded-lg hover:bg-gray-500 hover:bg-opacity-10` + + return ( +
  • + {child.to.startsWith('http') ? ( + + {child.label} + + ) : ( + { + detailsRef.current.removeAttribute('open') + }} + activeOptions={{ + exact: true, + includeHash: false, + includeSearch: false, + }} + className="!cursor-pointer relative" + > + {(props) => { + return ( +
    +
    + {/*
    */} + {child.label} + {/*
    */} +
    + {child.badge ? ( +
    + {child.badge} +
    + ) : null} +
    + ) + }} + + )} +
  • + ) + })} +
+ + ) + }) + + const logo = ( + + ) + + const smallMenu = ( +
+
+ +
+ + + {logo} +
+
+
+
+ + +
+ + {menuItems} +
+
+
+ ) + + const largeMenu = ( +
+
+ {logo} +
+
+ +
+
+ + +
+
+ {menuItems} +
+
+ ) + + return ( +
+ {smallMenu} + {largeMenu} +
+
+ {children} +
+
+ +
+
+
+ {prevItem ? ( + +
+ + {prevItem.label} +
+ + ) : null} +
+
+ {nextItem ? ( + +
+ + {nextItem.label} + {' '} + +
+ + ) : null} +
+
+
+
+
+
+
+ Our Partners +
+ {!partners.some((d) => d.libraries?.includes(libraryId as any)) ? ( + + ) : ( + partners + .filter((d) => d.sidebarImgLight) + .filter((d) => d.libraries?.includes(libraryId as any)) + .map((partner) => { + return ( +
+ +
+ {partner.name} + {partner.name} +
+
+ {partner.sidebarAfterImg || null} +
+ ) + }) + )} +
+ {libraryId === 'query' ? ( +
+ +
+ ) : null} + +
+ +
+ +
+ +
+ + {/*
+ +
*/} + + {libraryId !== 'query' ? ( +
+ +
+ ) : null} +
+
+ {showBytes ? ( +
+
+ {libraryId === 'query' ? ( + + ) : ( + + )} + +
+
+ ) : ( + + )} +
+ ) +} diff --git a/app/components/DocsLogo.tsx b/app/components/DocsLogo.tsx new file mode 100644 index 000000000..d011f4a6a --- /dev/null +++ b/app/components/DocsLogo.tsx @@ -0,0 +1,39 @@ +import { Link } from '@tanstack/react-router' +import { ThemeToggle } from './ThemeToggle' + +type Props = { + name: string + libraryId: string + version: string + colorFrom: string + colorTo: string +} + +export const DocsLogo = ({ + name, + version, + colorFrom, + colorTo, + libraryId, +}: Props) => { + const gradientText = `inline-block text-transparent bg-clip-text bg-gradient-to-r ${colorFrom} ${colorTo}` + + return ( + <> + + TanStack + + + {name}{' '} + {version} + +
+ +
+ + ) +} diff --git a/src/components/FileExplorer.tsx b/app/components/FileExplorer.tsx similarity index 80% rename from src/components/FileExplorer.tsx rename to app/components/FileExplorer.tsx index 13d36fc85..dbb85812e 100644 --- a/src/components/FileExplorer.tsx +++ b/app/components/FileExplorer.tsx @@ -6,11 +6,8 @@ import htmlIconUrl from '~/images/file-icons/html.svg?url' import jsonIconUrl from '~/images/file-icons/json.svg?url' import svelteIconUrl from '~/images/file-icons/svelte.svg?url' import vueIconUrl from '~/images/file-icons/vue.svg?url' -import markoIconUrl from '~/images/file-icons/marko.svg?url' -import emberIconUrl from '~/images/ember-logo.svg?url' import textIconUrl from '~/images/file-icons/txt.svg?url' import type { GitHubFileNode } from '~/utils/documents.server' -import { twMerge } from 'tailwind-merge' const getFileIconPath = (filename: string) => { const ext = filename.split('.').pop()?.toLowerCase() || '' @@ -32,11 +29,6 @@ const getFileIconPath = (filename: string) => { return svelteIconUrl case 'vue': return vueIconUrl - case 'marko': - return markoIconUrl - case 'gjs': - case 'gts': - return emberIconUrl default: return textIconUrl } @@ -142,7 +134,7 @@ export function FileExplorer({ } } return expanded - }, + } ) const startResizeRef = React.useRef({ @@ -218,7 +210,7 @@ export function FileExplorer({ width: isSidebarOpen ? sidebarWidth : 0, paddingRight: isSidebarOpen ? '0.5rem' : 0, }} - className={`shrink-0 overflow-y-auto bg-linear-to-r from-gray-50 via-gray-50 to-transparent dark:from-gray-800/50 dark:via-gray-800/50 dark:to-transparent shadow-sm ${ + className={`flex-shrink-0 overflow-y-auto bg-gradient-to-r from-gray-50 via-gray-50 to-transparent dark:from-gray-800/50 dark:via-gray-800/50 dark:to-transparent shadow-sm ${ isResizing ? '' : 'transition-all duration-300' }`} > @@ -236,7 +228,6 @@ export function FileExplorer({
) : null}
- {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
void }) => { - const pendingPrefetchesRef = React.useRef>({}) - - const cancelPrefetch = (path: string) => { - const timeoutId = pendingPrefetchesRef.current[path] - - if (timeoutId !== undefined) { - window.clearTimeout(timeoutId) - delete pendingPrefetchesRef.current[path] - } - } - - const schedulePrefetch = (path: string) => { - cancelPrefetch(path) - - pendingPrefetchesRef.current[path] = window.setTimeout(() => { - delete pendingPrefetchesRef.current[path] - props.prefetchFileContent(path) - }, 180) - } - - React.useEffect(() => { - const pendingPrefetches = pendingPrefetchesRef.current - - return () => { - for (const timeoutId of Object.values(pendingPrefetches)) { - window.clearTimeout(timeoutId) - } - } - }, []) - if (!props.files) return null return (