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..e703edcd4 --- /dev/null +++ b/.github/workflows/pr.yaml @@ -0,0 +1,27 @@ +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@v2 + with: + version: 8 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 18 + 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..d2ac7f7b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,39 +1,16 @@ node_modules package-lock.json yarn.lock -drizzle/migrations -.tanstack .DS_Store .cache .env -.env.* .vercel .output .vinxi -.tanstack-start/build -.nitro/* -.netlify/* -.wrangler/* /build/ /api/ /server/build /public/build - -# Sentry Config File -.env.sentry-build-plugin -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/ +.vinxi \ No newline at end of file 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 deleted file mode 100644 index d9b042470..000000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -v25.9.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..fd1b50a53 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +**/api +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts \ 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..eb580a5bf 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 Vercel! -### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/) - -
+- [Vercel](https://vercel.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.mjs b/app.config.mjs new file mode 100644 index 000000000..6ea1e924a --- /dev/null +++ b/app.config.mjs @@ -0,0 +1,128 @@ +import { createApp } from 'vinxi' +import reactRefresh from '@vitejs/plugin-react' +import { serverFunctions } from '@vinxi/server-functions/plugin' +import { TanStackRouterVite } from '@tanstack/router-vite-plugin' +import { config } from 'vinxi/plugins/config' +import tsconfigPaths from 'vite-tsconfig-paths' +import { serverTransform } from '@vinxi/server-functions/server' +import { normalize } from 'vinxi/lib/path' +import { resolve } from 'import-meta-resolve' +import path from 'path' +import { fileURLToPath } from 'url' + +const customVite = () => + config('dev', (router, app, env) => ({ + // ssr: { + // noExternal: [/react-router-server\/dist\/esm\/server-runtime/], + // }, + optimizeDeps: { + include: [ + 'node_modules@tanstack/react-router-server/**/*.js', + 'react-icons', + ], + }, + resolve: + env.command !== 'build' + ? { + dedupe: [ + 'react', + 'react-dom', + '@tanstack/store', + '@tanstack/react-store', + '@tanstack/react-router', + '@tanstack/react-router-server', + '@tanstack/react-cross-context', + '@tanstack/history', + 'use-sync-external-store', + ], + } + : {}, + // plugins: [ + // { + // name: 'inline-env-vars-as-prefix', + // // Write the env vars for some specific keys into the bundle at the very beginning of the file + // // using a (globalThis || window).tsr_env object. + // intro: `(globalThis || window).ROUTER_NAME = import.meta.env.ROUTER_NAME`, + // }, + // ], + })) + +export default createApp({ + server: { + preset: 'vercel', + experimental: { + asyncStorage: true, + asyncContext: true, + }, + }, + routers: [ + { + name: 'public', + type: 'static', + dir: './public', + base: '/', + }, + { + name: 'ssr', + type: 'http', + handler: './app/server.tsx', + target: 'server', + plugins: () => [ + TanStackRouterVite({ + experimental: { + enableCodeSplitting: true, + }, + }), + customVite(), + tsconfigPaths(), + serverTransform({ + runtime: `@tanstack/react-router-server/server-runtime`, + }), + ], + link: { + client: 'client', + }, + }, + { + name: 'client', + type: 'client', + handler: './app/client.tsx', + target: 'browser', + base: '/_build', + plugins: () => [ + TanStackRouterVite({ + experimental: { + enableCodeSplitting: true, + }, + }), + customVite(), + tsconfigPaths(), + serverFunctions.client({ + runtime: `@tanstack/react-router-server/client-runtime`, + }), + reactRefresh(), + ], + }, + serverFunctions.router({ + name: 'server', + plugins: () => [customVite(), tsconfigPaths()], + handler: resolveToRelative( + '@tanstack/react-router-server/server-handler' + ), + runtime: `@tanstack/react-router-server/server-runtime`, + }), + ], +}) + +function resolveToRelative(p) { + const toAbsolute = (file) => file.split('://').at(-1) + + const resolved = toAbsolute(resolve(p, import.meta.url)) + + const relative = path.relative( + path.resolve(toAbsolute(import.meta.url), '..'), + resolved + ) + + return relative +} 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 79% rename from src/blog/ag-grid-partnership.md rename to app/blog/ag-grid-partnership.md index 8dcf3e1d6..87477c72b 100644 --- a/src/blog/ag-grid-partnership.md +++ b/app/blog/ag-grid-partnership.md @@ -1,11 +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 -authors: - - Tanner Linsley - - Niall Crosby +published: 6/17/2022 --- We're excited to announce that [AG Grid](https://ag-grid.com/react-data-grid/?utm_source=reacttable&utm_campaign=githubreacttable) is now the official **TanStack Table** open-source partner! Together we will strive to achieve the following goals: @@ -16,7 +11,7 @@ We're excited to announce that [AG Grid](https://ag-grid.com/react-data-grid/?ut TanStack Table and [AG Grid](https://ag-grid.com/react-data-grid/?utm_source=reacttable&utm_campaign=githubreacttable) share the same general problem space, but are implemented via drastically different architectures and paradigms, each offering unique trade-offs, opinions and optimizations depending on use-case. These differences and trade-offs are complimentary to one another and together form what we believe are the best two datagrid/table options available in the JavaScript and TypeScript ecosystem. -To learn about the differences and trade-offs between the two libraries, start by reading the [introduction to TanStack Table](/table/v8/docs/introduction) and the [introduction to AG Grid](https://ag-grid.com/react-data-grid/?utm_source=reacttable&utm_campaign=githubreacttable)! +To learn about the differences and trade-offs between the two libraries, start by reading the [introduction to TanStack Table](/table/v8/docs/guide/introduction) and the [introduction to AG Grid](https://ag-grid.com/react-data-grid/?utm_source=reacttable&utm_campaign=githubreacttable)! We are excited about the future of datagrids and tables and we at TanStack are honored that AG Grid is invested in the success of it's open source ecosystem! diff --git a/src/blog/announcing-tanstack-query-v4.md b/app/blog/announcing-tanstack-query-v4.md similarity index 92% rename from src/blog/announcing-tanstack-query-v4.md rename to app/blog/announcing-tanstack-query-v4.md index 2094f1ad3..ffc8b18fd 100644 --- a/src/blog/announcing-tanstack-query-v4.md +++ b/app/blog/announcing-tanstack-query-v4.md @@ -1,10 +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 -authors: - - Dominik Dorfmeister +published: 7/14/2022 --- We're excited to announce the next version of [TanStack Query](/query/v4), previously known as `react-query` 🎉. diff --git a/src/blog/announcing-tanstack-query-v5.md b/app/blog/announcing-tanstack-query-v5.md similarity index 93% rename from src/blog/announcing-tanstack-query-v5.md rename to app/blog/announcing-tanstack-query-v5.md index b061bdea3..7f006138b 100644 --- a/src/blog/announcing-tanstack-query-v5.md +++ b/app/blog/announcing-tanstack-query-v5.md @@ -1,10 +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 -authors: - - Dominik Dorfmeister +published: 10/17/2023 --- About one year ago, we announced the [TanStack Query v5 roadmap](https://github.com/TanStack/query/discussions/4252), and the whole team has been working hard on that version ever since. So we're super happy to announce that today is the day: After 91 alpha releases, 35 betas and 16 release candidates, TanStack Query [v5.0.0](https://github.com/TanStack/query/releases/tag/v5.0.0) is finally here! 🎉 @@ -15,7 +11,7 @@ v5 continues the journey of v4, trying to make TanStack Query smaller (v5 is ~20 As a big breaking change, we've removed most overloads from the codebase, unifying how you use `useQuery` and other hooks. This is something we wanted to do for v4, but a [TypeScript limitation](https://github.com/microsoft/TypeScript/issues/43371) prevented us from doing that. TypeScript addressed this issue in TS 4.7, so we were able to remove all the overloads that we had for calling `useQuery` with a different amount of parameters. This is a huge DX win, because methods with overloads usually have quite bad TypeScript error messages. -This is the biggest breaking change in v5, but we think it's worth it. The API is now much more consistent - you always just pass _one_ object. To alleviate the pain of changing all occurrences manually, we have tried to prepare everyone for this coming change for the last months. The documentation was changed to use the new API, and we released an auto-fixable [eslint rule](/query/v4/docs/eslint/prefer-query-object-syntax) in our eslint package. Additionally, v5 comes with [a codemod](/query/v5/docs/react/guides/migrating-to-v5#codemod) to help with the transition. +This is the biggest breaking change in v5, but we think it's worth it. The API is now much more consistent - you always just pass _one_ object. To alleviate the pain of changing all occurrences manually, we have tried to prepare everyone for this coming change for the last months. The documentation was changed to use the new API, and we released an auto-fixable [eslint rule](/query/v4/docs/react/eslint/prefer-query-object-syntax) in our eslint package. Additionally, v5 comes with [a codemod](/query/v5/docs/react/guides/migrating-to-v5#codemod) to help with the transition. Apart from that, we've renamed `cacheTime` to `gcTime` to better reflect what it is doing, merged `keepPreviousData` with `placeholderData`, renamed `loading` states to `pending` and [removed the callbacks](https://github.com/TanStack/query/discussions/5279) from `useQuery`. All these changes make v5 the most consistent and best version for new starters. diff --git a/app/client.tsx b/app/client.tsx new file mode 100644 index 000000000..42b92f12f --- /dev/null +++ b/app/client.tsx @@ -0,0 +1,13 @@ +/// +import { hydrateRoot } from 'react-dom/client' +import 'vinxi/client' + +import { createRouter } from './router' +import { StartClient } from '@tanstack/react-router-server' + +const router = createRouter() + +const app = + +router.hydrate() +hydrateRoot(document, app) diff --git a/app/components/BytesForm.tsx b/app/components/BytesForm.tsx new file mode 100644 index 000000000..0df9179b1 --- /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..236b97a3b --- /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/app/components/CodeBlock.tsx b/app/components/CodeBlock.tsx new file mode 100644 index 000000000..e7d2f78cb --- /dev/null +++ b/app/components/CodeBlock.tsx @@ -0,0 +1,91 @@ +import { useState, type ReactNode } from 'react' +import invariant from 'tiny-invariant' +import type { Language } from 'prism-react-renderer' +import { Highlight, Prism } from 'prism-react-renderer' +import { FaCopy } from 'react-icons/fa' +import { svelteHighlighter } from '~/utils/svelteHighlighter' +// Add back additional language support after `prism-react` upgrade +;(typeof global !== 'undefined' ? global : window).Prism = Prism +// @ts-expect-error +import('prismjs/components/prism-diff') +// @ts-expect-error +import('prismjs/components/prism-bash') + +// @ts-ignore Alias markup as vue highlight +Prism.languages.vue = Prism.languages.markup + +// Enable svelte syntax highlighter +svelteHighlighter() + +function getLanguageFromClassName(className: string) { + const match = className.match(/language-(\w+)/) + return match ? match[1] : '' +} + +function isLanguageSupported(lang: string): lang is Language { + return lang in Prism.languages +} + +type Props = { + children: ReactNode +} + +export const CodeBlock = ({ children }: Props) => { + invariant(!!children, 'children is required') + const [copied, setCopied] = useState(false) + const child = Array.isArray(children) ? children[0] : children + const className = child.props.className || '' + const userLang = getLanguageFromClassName(className) + const lang = isLanguageSupported(userLang) ? userLang : 'bash' + const code = Array.isArray(child.props.children) + ? child.props.children[0] + : child.props.children + return ( +
+ +
+
+ {lang} +
+
+ + {({ className, tokens, getLineProps, getTokenProps }) => ( +
+                
+                  {tokens.map((line, i) => (
+                    
+ {line.map((token, key) => ( + + ))} +
+ ))} +
+
+ )} +
+
+
+
+ ) +} diff --git a/src/components/DefaultCatchBoundary.tsx b/app/components/DefaultCatchBoundary.tsx similarity index 65% rename from src/components/DefaultCatchBoundary.tsx rename to app/components/DefaultCatchBoundary.tsx index 5083603cd..5ca2c5f1f 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, @@ -35,7 +24,7 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) { console.error(error) return ( -
+

{/*
{status}
{statusText ? ( @@ -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..f93470413 --- /dev/null +++ b/app/components/Doc.tsx @@ -0,0 +1,40 @@ +import { FaEdit } from 'react-icons/fa' +import { DocTitle } from '~/components/DocTitle' +import { RenderMarkdown } from '~/components/RenderMarkdown' + +export function Doc({ + title, + content, + repo, + branch, + filePath, +}: { + title: string + content: string + repo: string + branch: string + filePath: string +}) { + return ( +
+ {title ? {title} : null} +
+
+
+
+ {content} +
+
+
+ +
+
+ ) +} diff --git a/app/components/DocSearch.tsx b/app/components/DocSearch.tsx new file mode 100644 index 000000000..e990b5737 --- /dev/null +++ b/app/components/DocSearch.tsx @@ -0,0 +1,14 @@ +// During SSR, we need to double unpack the default export. +// On the client in the browser, we can rely on vite to handle this for us. + +import * as pkg from '@docsearch/react' + +let DocSearch: typeof pkg.DocSearch + +if (import.meta.env.SSR) { + DocSearch = (pkg as unknown as { default: any }).default.DocSearch +} else { + DocSearch = pkg.DocSearch +} + +export { DocSearch } 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..32548dd99 --- /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..8b124f9b4 --- /dev/null +++ b/app/components/DocsCalloutQueryGG.tsx @@ -0,0 +1,25 @@ +import { LogoQueryGGSmall } from '~/components/LogoQueryGGSmall' + +export function DocsCalloutQueryGG(props: React.HTMLProps) { + return ( +
+
+ Want to Skip the Docs? +
+ +
+ “This course is the best way to learn how to use React Query in + real-world applications.” + —Tanner Linsley +
+ + Check it out + +
+ ) +} diff --git a/app/components/DocsLayout.tsx b/app/components/DocsLayout.tsx new file mode 100644 index 000000000..c47543982 --- /dev/null +++ b/app/components/DocsLayout.tsx @@ -0,0 +1,624 @@ +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 { Carbon } from '~/components/Carbon' +import { Select } from '~/components/Select' +import { useLocalStorage } from '~/utils/useLocalStorage' +import { DocsCalloutQueryGG } from '~/components/DocsCalloutQueryGG' +import { DocsCalloutBytes } from '~/components/DocsCalloutBytes' +import { DocsLogo } from '~/components/DocsLogo' +import { last } from '~/utils/utils' +import type { SelectOption } from '~/components/Select' +import type { ConfigSchema, MenuItem } from '~/utils/config' +import { create } from 'zustand' +import { DocSearch } from './DocSearch' +import { Framework, getFrameworkOptions } from '~/libraries' + +// 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, + experimental_returnIntersection: true, + }) + + 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: Record) => ({ + ...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, + experimental_returnIntersection: true, + }) + + 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[] +}) => { + const currentFramework = useCurrentFramework(frameworks) + + const localMenu: MenuItem = { + label: 'Menu', + children: [ + { + label: 'Home', + to: '..', + }, + { + 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) => { + 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, + } + }), + ].filter(Boolean) +} + +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 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 menuItems = menuConfig.map((group, i) => { + return ( +
+
{group?.label}
+
+
+ {group?.children?.map((child, i) => { + const linkClasses = `flex gap-2 items-center justify-between group px-2 py-1 rounded-lg hover:bg-gray-500 hover:bg-opacity-10` + + return ( +
+ {child.to.startsWith('http') ? ( + + {child.label} + + ) : ( + { + detailsRef.current.removeAttribute('open') + }} + activeOptions={{ + exact: true, + }} + > + {(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} +
+
+
+
+ {config?.docSearch?.indexName?.includes('query') ? ( + + ) : ( + + )} +
+ {showBytes ? ( +
+
+ {config?.docSearch?.indexName?.includes('query') ? ( + + ) : ( + + )} + +
+
+ ) : ( + + )} +
+
+ ) +} diff --git a/app/components/DocsLogo.tsx b/app/components/DocsLogo.tsx new file mode 100644 index 000000000..720220167 --- /dev/null +++ b/app/components/DocsLogo.tsx @@ -0,0 +1,27 @@ +import { Link } from '@tanstack/react-router' + +type Props = { + name: string + linkTo: string + version: string + colorFrom: string + colorTo: string +} + +export const DocsLogo = (props: Props) => { + const { name, version, colorFrom, colorTo } = 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/Footer.tsx b/app/components/Footer.tsx similarity index 55% rename from src/components/Footer.tsx rename to app/components/Footer.tsx index 0e56901e2..0949fb744 100644 --- a/src/components/Footer.tsx +++ b/app/components/Footer.tsx @@ -1,45 +1,29 @@ import { Link } from '@tanstack/react-router' -import { Card } from './Card' const footerLinks = [ { label: 'Blog', to: '/blog' }, - { label: '@Tan_Stack on X.com', to: 'https://x.com/tan_stack' }, + { label: '@Tan_Stack Twitter', to: 'https://twitter.com/tan_stack' }, { - label: '@TannerLinsley on X.com', - to: 'https://x.com/tannerlinsley', + label: '@TannerLinsley Twitter', + to: 'https://twitter.com/tannerlinsley', }, { label: 'GitHub', to: 'https://github.com/tanstack' }, { - label: 'YouTube', - to: 'https://youtube.com/@tan_stack', + label: 'Youtube', + to: 'https://www.youtube.com/user/tannerlinsley', }, { label: 'Nozzle.io - Keyword Rank Tracker', to: 'https://nozzle.io', }, - { - label: 'Ethos', - to: '/ethos', - }, - { - label: 'Tenets', - to: '/tenets', - }, - { - label: 'Privacy Policy', - to: '/privacy', - }, - { - label: 'Terms of Service', - to: '/terms', - }, ] export function Footer() { return ( -
{footerLinks.map((item) => ( @@ -54,9 +38,9 @@ export function Footer() {
))}
-
+
© {new Date().getFullYear()} TanStack LLC
- +
) } diff --git a/src/components/Logo.tsx b/app/components/Logo.tsx similarity index 99% rename from src/components/Logo.tsx rename to app/components/Logo.tsx index 509aade55..ee17d86ff 100644 --- a/src/components/Logo.tsx +++ b/app/components/Logo.tsx @@ -1,8 +1,6 @@ -import { BrandContextMenu } from '~/components/BrandContextMenu' - export function Logo(props: React.HTMLProps) { return ( - +
) { - +
) } diff --git a/src/components/LogoColor.tsx b/app/components/LogoColor.tsx similarity index 99% rename from src/components/LogoColor.tsx rename to app/components/LogoColor.tsx index 48175d645..4cfb79f07 100644 --- a/src/components/LogoColor.tsx +++ b/app/components/LogoColor.tsx @@ -1,8 +1,6 @@ -import { BrandContextMenu } from '~/components/BrandContextMenu' - export function LogoColor(props: React.HTMLProps) { return ( - +
) { - +
) } diff --git a/src/components/LogoQueryGG.tsx b/app/components/LogoQueryGG.tsx similarity index 98% rename from src/components/LogoQueryGG.tsx rename to app/components/LogoQueryGG.tsx index bfc3c6070..207f4e681 100644 --- a/src/components/LogoQueryGG.tsx +++ b/app/components/LogoQueryGG.tsx @@ -213,7 +213,7 @@ export function LogoQueryGG(props: React.HTMLProps) {
diff --git a/src/components/LogoQueryGGSmall.tsx b/app/components/LogoQueryGGSmall.tsx similarity index 97% rename from src/components/LogoQueryGGSmall.tsx rename to app/components/LogoQueryGGSmall.tsx index 56d00f537..0ff66c196 100644 --- a/src/components/LogoQueryGGSmall.tsx +++ b/app/components/LogoQueryGGSmall.tsx @@ -220,7 +220,7 @@ export function LogoQueryGGSmall(props: React.HTMLProps) {
diff --git a/app/components/MarkdownLink.tsx b/app/components/MarkdownLink.tsx new file mode 100644 index 000000000..23d4acbdc --- /dev/null +++ b/app/components/MarkdownLink.tsx @@ -0,0 +1,18 @@ +import { Link } from '@tanstack/react-router' +import type { HTMLProps } from 'react' + +export function MarkdownLink(props: HTMLProps) { + if ((props as { href: string }).href?.startsWith('http')) { + return + } + + return ( + + ) +} diff --git a/app/components/NotFound.tsx b/app/components/NotFound.tsx new file mode 100644 index 000000000..db30a81df --- /dev/null +++ b/app/components/NotFound.tsx @@ -0,0 +1,28 @@ +import { Link } from '@tanstack/react-router' + +export function NotFound({ children }: { children?: any }) { + return ( +
+

404 Not Found

+

+ The page you are looking for does not exist. +

+ {children || ( +

+ + + Start Over + +

+ )} +
+ ) +} diff --git a/app/components/QueryGGBanner.tsx b/app/components/QueryGGBanner.tsx new file mode 100644 index 000000000..2c3290af4 --- /dev/null +++ b/app/components/QueryGGBanner.tsx @@ -0,0 +1,34 @@ +import { IoIosClose } from 'react-icons/io' +import { useLocalStorage } from '~/utils/useLocalStorage' +import { useClientOnlyRender } from '~/utils/useClientOnlyRender' + +export function QueryGGBanner() { + const [hidden, setHidden] = useLocalStorage('pppbanner-hidden', false) + + if (!useClientOnlyRender()) { + return null + } + + return ( + <> + {!hidden && ( +
+

+ Want to skip the docs? Check out{' '} + + query.gg + {' '} + – the simplest way to master React Query. +

+ +
+ )} + + ) +} diff --git a/app/components/RedirectVersionBanner.tsx b/app/components/RedirectVersionBanner.tsx new file mode 100644 index 000000000..eb1389e84 --- /dev/null +++ b/app/components/RedirectVersionBanner.tsx @@ -0,0 +1,55 @@ +import { useLocalStorage } from '~/utils/useLocalStorage' +import { useClientOnlyRender } from '~/utils/useClientOnlyRender' +import { Link } from '@tanstack/react-router' + +export function RedirectVersionBanner(props: { + version: string + latestVersion: string +}) { + const { version, latestVersion } = props + + // After user clicks hide, do not show modal for a month, and then remind users that there is a new version! + const [showModal, setShowModal] = useLocalStorage( + 'showRedirectToLatestModal', + true, + 1000 * 60 * 24 * 30 + ) + + if (!useClientOnlyRender()) { + return null + } + + if (![latestVersion, 'latest'].includes(version) && showModal) { + return ( +
+
+ You are currently reading {version} docs. Redirect to{' '} + + latest + {' '} + version? +
+ + Latest + + +
+ ) + } +} diff --git a/app/components/RenderMarkdown.tsx b/app/components/RenderMarkdown.tsx new file mode 100644 index 000000000..11cbf3554 --- /dev/null +++ b/app/components/RenderMarkdown.tsx @@ -0,0 +1,76 @@ +import { CodeBlock } from '~/components/CodeBlock' +import { MarkdownLink } from '~/components/MarkdownLink' +import type { FC, HTMLProps } from 'react' +import ReactMarkdown from 'react-markdown' +import rehypeSlug from 'rehype-slug' +import remarkGfm from 'remark-gfm' +import rehypeRaw from 'rehype-raw' + +const CustomHeading = ({ + Comp, + id, + ...props +}: HTMLProps & { + Comp: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' +}) => { + if (id) { + return ( + + + + ) + } + return +} + +const makeHeading = + (type: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6') => + (props: HTMLProps) => + ( + + ) + +const defaultComponents: Record = { + a: MarkdownLink, + pre: CodeBlock, + h1: makeHeading('h1'), + h2: makeHeading('h2'), + h3: makeHeading('h3'), + h4: makeHeading('h4'), + h5: makeHeading('h5'), + h6: makeHeading('h6'), + iframe: (props) => +
+ +
+
+ Wow, you've come a long way! +
+
+ Only one thing left to do... +
+
+ + Get Started! + +
+
+
+
+ + ) +} diff --git a/app/routes/_libraries.query.$version.index.tsx b/app/routes/_libraries.query.$version.index.tsx new file mode 100644 index 000000000..09cff94df --- /dev/null +++ b/app/routes/_libraries.query.$version.index.tsx @@ -0,0 +1,571 @@ +import * as React from 'react' + +import { CgCornerUpLeft, CgSpinner } from 'react-icons/cg' +import { + FaBolt, + FaBook, + FaCheckCircle, + FaCogs, + FaDiscord, + FaGithub, + FaTshirt, +} from 'react-icons/fa' +import { Await, Link, getRouteApi } from '@tanstack/react-router' +import { Carbon } from '~/components/Carbon' +import { Footer } from '~/components/Footer' +import { VscPreview, VscWand } from 'react-icons/vsc' +import { TbHeartHandshake } from 'react-icons/tb' +import SponsorPack from '~/components/SponsorPack' +import { QueryGGBanner } from '~/components/QueryGGBanner' +import { queryProject } from '~/libraries/query' +import { LogoQueryGG } from '~/components/LogoQueryGG' +import { createFileRoute } from '@tanstack/react-router' +import { Framework, getBranch } from '~/libraries' +import { seo } from '~/utils/seo' + +const menu = [ + { + label: ( +
+ TanStack +
+ ), + to: '/', + }, + { + label: ( +
+ Examples +
+ ), + to: './docs/framework/react/examples/basic', + }, + { + label: ( +
+ Docs +
+ ), + to: './docs/', + }, + { + label: ( +
+ GitHub +
+ ), + to: `https://github.com/${queryProject.repo}`, + }, + { + label: ( +
+ Discord +
+ ), + to: 'https://tlinz.com/discord', + }, + { + label: ( +
+ Merch +
+ ), + to: `https://cottonbureau.com/people/tanstack`, + }, +] + +export const Route = createFileRoute('/_libraries/query/$version/')({ + component: VersionIndex, + meta: () => + seo({ + title: queryProject.name, + description: queryProject.description, + }), +}) + +const librariesRouteApi = getRouteApi('/_libraries') + +export default function VersionIndex() { + const { sponsorsPromise } = librariesRouteApi.useLoaderData() + const { version } = Route.useParams() + const branch = getBranch(queryProject, version) + const [framework, setFramework] = React.useState('react') + const [isDark, setIsDark] = React.useState(true) + + React.useEffect(() => { + setIsDark(window.matchMedia?.(`(prefers-color-scheme: dark)`).matches) + }, []) + + const gradientText = `inline-block text-transparent bg-clip-text bg-gradient-to-r ${queryProject.colorFrom} ${queryProject.colorTo}` + + return ( +
+ +
+
+
+ {menu?.map((item, i) => { + const label = ( +
+ {item.label} +
+ ) + + return ( +
+ {item.to.startsWith('http') ? ( + {label} + ) : ( + + {label} + + )} +
+ ) + })} +
+
+
+

+ TanStack Query{' '} + + {version === 'latest' ? queryProject.latestVersion : version} + +

+
+

+ Powerful{' '} + + asynchronous state management + {' '} + for TS/JS, React, Solid, Vue, Svelte and Angular +

+

+ Toss out that granular state management, manual refetching and + endless bowls of async-spaghetti code. TanStack Query gives you + declarative, always-up-to-date auto-managed queries and mutations + that{' '} + + directly improve both your developer and user experiences + + . +

+ + Read the Docs + +

+ (or check out{' '} + + query.gg + {' '} + – the official React Query course) +

+
+
+
+ +
+

+ Declarative & Automatic +

+

+ Writing your data fetching logic by hand is over. Tell + TanStack Query where to get your data and how fresh you need + it to be and the rest is automatic. It handles{' '} + + caching, background updates and stale data out of the box + with zero-configuration + + . +

+
+
+
+
+ +
+
+

+ Simple & Familiar +

+

+ If you know how to work with promises or async/await, then you + already know how to use TanStack Query. There's{' '} + + no global state to manage, reducers, normalization systems + or heavy configurations to understand + + . Simply pass a function that resolves your data (or throws an + error) and the rest is history. +

+
+
+
+
+ +
+
+

+ Extensible +

+

+ TanStack Query is configurable down to each observer instance + of a query with knobs and options to fit every use-case. It + comes wired up with{' '} + + dedicated devtools, infinite-loading APIs, and first class + mutation tools that make updating your data a breeze + + . Don't worry though, everything is pre-configured for + success! +

+
+
+
+ +
+
+ +
+
+
+ Created by{' '} + + Dominik Dorfmeister + {' '} + and{' '} + + ui.dev + +
+
+ +
+ “This is the best way to learn how to use React Query in + real-world applications.” +
—Tanner Linsley
+
+ +
+
+ + + + Save time by learning with a guided approach +
+
+ + + + Get hands-on experience building a real-world application +
+
+ + + + Never worry about data fetching again +
+
+ + Check it out + +
+
+
+ +
+
+

+ No dependencies. All the Features. +

+

+ With zero dependencies, TanStack Query is extremely lean given + the dense feature set it provides. From weekend hobbies all the + way to enterprise e-commerce systems (Yes, I'm lookin' at you + Walmart! 😉), TanStack Query is the battle-hardened tool to help + you succeed at the speed of your creativity. +

+
+
+ {[ + 'Backend agnostic', + 'Dedicated Devtools', + 'Auto Caching', + 'Auto Refetching', + 'Window Focus Refetching', + 'Polling/Realtime Queries', + 'Parallel Queries', + 'Dependent Queries', + 'Mutations API', + 'Automatic Garbage Collection', + 'Paginated/Cursor Queries', + 'Load-More/Infinite Scroll Queries', + 'Scroll Recovery', + 'Request Cancellation', + 'Suspense Ready!', + 'Render-as-you-fetch', + 'Prefetching', + 'Variable-length Parallel Queries', + 'Offline Support', + 'SSR Support', + 'Data Selectors', + ].map((d, i) => { + return ( + + {d} + + ) + })} +
+
+ +
+
+ Trusted in Production by +
+ {/* @ts-ignore */} + +
+ {(new Array(4) as string[]) + .fill('') + .reduce( + (all) => [...all, ...all], + [ + 'Google', + 'Walmart', + 'Facebook', + 'PayPal', + 'Amazon', + 'American Express', + 'Microsoft', + 'Target', + 'Ebay', + 'Autodesk', + 'CarFAX', + 'Docusign', + 'HP', + 'MLB', + 'Volvo', + 'Ocado', + 'UPC.ch', + 'EFI.com', + 'ReactBricks', + 'Nozzle.io', + 'Uber', + ] + ) + .map((d, i) => ( + + {d} + + ))} +
+ {/* @ts-ignore */} +
+
+ +
+

+ Partners +

+
+
+ + Query You? + +
+
+ We're looking for a TanStack Query OSS Partner to go above and + beyond the call of sponsorship. Are you as invested in + TanStack Query as we are? Let's push the boundaries of Query + together! +
+ + Let's chat + +
+
+
+ +
+

+ Sponsors +

+
+ } + children={(sponsors) => { + return + }} + /> +
+ +
+ +
+
+ +
+ + This ad helps us be happy about our invested time and not burn out + and rage-quit OSS. Yay money! 😉 + +
+ +
+
+

+ Less code, fewer edge cases. +

+

+ Instead of writing reducers, caching logic, timers, retry logic, + complex async/await scripting (I could keep going...), you + literally write a tiny fraction of the code you normally would. + You will be surprised at how little code you're writing or how + much code you're deleting when you use TanStack Query. Try it + out with one of the examples below! +

+
+ {( + [ + { label: 'Angular', value: 'angular' }, + { label: 'React', value: 'react' }, + { label: 'Solid', value: 'solid' }, + { label: 'Svelte', value: 'svelte' }, + { label: 'Vue', value: 'vue' }, + ] as const + ).map((item) => ( + + ))} +
+
+
+ + {[''].includes(framework) ? ( +
+
+ Looking for the @tanstack/{framework}-query{' '} + example? We could use your help to build the{' '} + @tanstack/{framework}-query adapter! Join the{' '} + + TanStack Discord Server + {' '} + and let's get to work! +
+
+ ) : ( +
+ +
+ )} + +
+
+ Wow, you've come a long way! +
+
+ Only one thing left to do... +
+
+ + Read the Docs! + +
+
+
+
+
+
+ ) +} diff --git a/app/routes/_libraries.ranger.$version.index.tsx b/app/routes/_libraries.ranger.$version.index.tsx new file mode 100644 index 000000000..2716518ff --- /dev/null +++ b/app/routes/_libraries.ranger.$version.index.tsx @@ -0,0 +1,377 @@ +import * as React from 'react' +import { CgCornerUpLeft, CgSpinner, CgTimelapse } from 'react-icons/cg' +import { + FaBook, + FaCheckCircle, + FaDiscord, + FaGithub, + FaTshirt, +} from 'react-icons/fa' +import { Await, Link } from '@tanstack/react-router' +import { TbHeartHandshake, TbZoomQuestion } from 'react-icons/tb' +import { VscPreview } from 'react-icons/vsc' +import { RiLightbulbFlashLine } from 'react-icons/ri' +import { rangerProject } from '~/libraries/ranger' +import { Carbon } from '~/components/Carbon' +import { Footer } from '~/components/Footer' +import SponsorPack from '~/components/SponsorPack' +import { createFileRoute } from '@tanstack/react-router' +import { getRouteApi } from '@tanstack/react-router' +import { Framework, getBranch } from '~/libraries' +import { seo } from '~/utils/seo' + +const menu = [ + { + label: ( +
+ TanStack +
+ ), + to: '/', + }, + { + label: ( +
+ Examples +
+ ), + to: './docs/framework/react/examples/basic', + }, + { + label: ( +
+ Docs +
+ ), + to: './docs/overview', + }, + { + label: ( +
+ GitHub +
+ ), + to: `https://github.com/${rangerProject.repo}`, + }, + { + label: ( +
+ Discord +
+ ), + to: 'https://tlinz.com/discord', + }, + { + label: ( +
+ Merch +
+ ), + to: `https://cottonbureau.com/people/tanstack`, + }, +] + +export const Route = createFileRoute('/_libraries/ranger/$version/')({ + component: VersionIndex, + meta: () => + seo({ + title: rangerProject.name, + description: rangerProject.description, + }), +}) + +const librariesRouteApi = getRouteApi('/_libraries') + +export default function VersionIndex() { + const { sponsorsPromise } = librariesRouteApi.useLoaderData() + const { version } = Route.useParams() + const branch = getBranch(rangerProject, version) + const [framework] = React.useState('react') + const [isDark, setIsDark] = React.useState(true) + + React.useEffect(() => { + setIsDark(window.matchMedia?.(`(prefers-color-scheme: dark)`).matches) + }, []) + + const gradientText = `inline-block text-transparent bg-clip-text bg-gradient-to-r ${rangerProject.colorFrom} ${rangerProject.colorTo}` + + return ( + <> +
+
+ {menu?.map((item, i) => { + const label = ( +
+ {item.label} +
+ ) + + return ( +
+ {item.to.startsWith('http') ? ( + {label} + ) : ( + + {label} + + )} +
+ ) + })} +
+
+

+ TanStack Ranger{' '} + + BETA + +

+

+ Modern and{' '} + + headless + {' '} + ranger ui library +

+

+ A fully typesafe ranger hooks for React. +

+ + Get Started + +
+
+
+
+ +
+
+

+ Typesafe & powerful, yet familiarly simple +

+

+ Hooks for building range and multi-range sliders in React{' '} + + 100% typesafe without compromising on DX + + . +

+
+
+
+
+ +
+
+

+ "Headless" UI library +

+

+ Headless and extensible. Ranger doesn't render or supply any + actual UI elements. It's a{' '} + + utility for building your own custom-designed UI components + + . +

+
+
+
+
+ +
+
+

Extensible

+

+ Designed with maximum inversion of control in mind, Ranger is + built to be{' '} + + easily extended and customized + {' '} + to fit your needs. +

+
+
+
+ +
+
+

+ Feature Rich and Lightweight Headless utility, which means out of + the box, it doesn't render or supply any actual UI elements. +

+

+ Behold, the obligatory feature-list: +

+
+
+ {[ + '100% Typesafe', + 'Lightweight (306 kB)', + 'Easy to maintain', + 'Extensible', + 'Not dictating UI', + ].map((d, i) => { + return ( + + {d} + + ) + })} +
+
+ +
+

+ Partners +

+
+
+ + Ranger You? + +
+
+ We're looking for a TanStack Ranger OSS Partner to go above and + beyond the call of sponsorship. Are you as invested in TanStack + Ranger as we are? Let's push the boundaries of Ranger together! +
+ + Let's chat + +
+
+
+ +
+

+ Sponsors +

+
+ } + children={(sponsors) => { + return + }} + /> +
+ +
+ +
+
+ +
+ + This ad helps us be happy about our invested time and not burn out + and rage-quit OSS. Yay money! 😉 + +
+ +
+
+

+ Take it for a spin! +

+

+ Let's see it in action! +

+
+
+ +
+ +
+ {/* )} */} + +
+
+ Wow, you've come a long way! +
+
+ Only one thing left to do... +
+
+ + Get Started! + +
+
+
+
+ + ) +} diff --git a/app/routes/_libraries.router.$version.index.tsx b/app/routes/_libraries.router.$version.index.tsx new file mode 100644 index 000000000..36c21d72e --- /dev/null +++ b/app/routes/_libraries.router.$version.index.tsx @@ -0,0 +1,426 @@ +import * as React from 'react' +import { CgSpinner, CgTimelapse } from 'react-icons/cg' +import { + FaBook, + FaCheckCircle, + FaDiscord, + FaGithub, + FaTshirt, +} from 'react-icons/fa' +import { + Await, + Link, + createFileRoute, + getRouteApi, +} from '@tanstack/react-router' +import { TbHeartHandshake, TbZoomQuestion } from 'react-icons/tb' +import { VscPreview } from 'react-icons/vsc' +import { RiLightbulbFlashLine } from 'react-icons/ri' +import { routerProject } from '~/libraries/router' +import { Carbon } from '~/components/Carbon' +import { Footer } from '~/components/Footer' +import SponsorPack from '~/components/SponsorPack' +import { Framework, getBranch } from '~/libraries' +import { seo } from '~/utils/seo' + +const menu = [ + { + label: ( +
+ Examples +
+ ), + to: './docs/framework/react/examples/kitchen-sink-file-based', + }, + { + label: ( +
+ Docs +
+ ), + to: './docs/framework/react/overview', + }, + { + label: ( +
+ GitHub +
+ ), + to: `https://github.com/${routerProject.repo}`, + }, + { + label: ( +
+ Discord +
+ ), + to: 'https://tlinz.com/discord', + }, + { + label: ( +
+ Merch +
+ ), + to: `https://cottonbureau.com/people/tanstack`, + }, +] + +export const Route = createFileRoute('/_libraries/router/$version/')({ + component: RouterVersionIndex, + meta: () => + seo({ + title: routerProject.name, + description: routerProject.description, + }), +}) + +const librariesRouteApi = getRouteApi('/_libraries') + +function RouterVersionIndex() { + const { sponsorsPromise } = librariesRouteApi.useLoaderData() + const { version } = Route.useParams() + const branch = getBranch(routerProject, version) + const [framework] = React.useState('react') + const [isDark, setIsDark] = React.useState(true) + + React.useEffect(() => { + setIsDark(window.matchMedia?.(`(prefers-color-scheme: dark)`).matches) + }, []) + + const gradientText = `inline-block text-transparent bg-clip-text bg-gradient-to-r ${routerProject.colorFrom} ${routerProject.colorTo}` + + return ( +
+
+ {menu?.map((item, i) => { + const label = ( +
{item.label}
+ ) + + return ( +
+ {item.to.startsWith('http') ? ( + {label} + ) : ( + + {label} + + )} +
+ ) + })} +
+
+
+

+ TanStack Router +

+
+

+ + Modern and scalable + {' '} + routing for React applications +

+

+ A fully type-safe React router with built-in data fetching, + stale-while revalidate caching and first-class search-param APIs. +

+ + Get Started + +
+
+
+
+ +
+
+

+ Typesafe & powerful, yet familiarly simple +

+

+ TanStack Router builds on modern routing patterns made popular by + other tools, but has been re-engineered from the ground up to be{' '} + + 100% typesafe without compromising on DX + + . You can have your cake and eat it too! +

+
+
+
+
+ +
+
+

+ Built-in Data Fetching with Caching +

+

+ Hoist your data fetching and avoid waterfalls with TanStack + Router's loader API and get{' '} + + instant navigations with built-in caching and automatic + preloading + + . Need something more custom? Router's API is{' '} + + designed to work with your favorite client-side cache libraries! + {' '} + Your users will notice the difference when your pages not only + load in parallel but also stay up to date over time. +

+
+
+
+
+ +
+
+

+ Search Param APIs to make your state-manager jealous +

+

+ Instead of throwing you to the URLSearchParam wolves, TanStack + Router outfits you with state-manager-grade search param APIs. + With{' '} + + schemas, validation, full type-safety and pre/post manipulation + {' '} + you'll wonder why you're not storing everything in the URL. + Goodbye in-memory state 👋! +

+
+
+
+ +
+
+

+ Feature Rich and Lightweight +

+

+ Behold, the obligatory feature-list: +

+
+
+ {[ + '100% Typesafe', + 'Parallel Route Loaders', + '1st-class Search Param APIs', + 'Nested/Layout Routes', + 'Lightweight (12kb)', + 'Suspense + Transitions', + 'Strict Navigation', + 'Auto-completed Paths', + 'Search Param Schemas', + 'Search Param Validation', + 'Search Param Parsing + Serialization', + 'Search Param Pre/Post Processing', + 'Structural Sharing', + 'Automatic Prefetching', + 'Asynchronous Elements', + 'Pending Elements', + 'Error Boundaries', + ].map((d, i) => { + return ( + + {d} + + ) + })} +
+
+ +
+
+ +
+ + This ad helps us be happy about our invested time and not burn out and + rage-quit OSS. Yay money! 😉 + +
+ +
+
+
+

+ Take it for a spin! +

+

+ Create a route, pop in a Router, and start slingin' some code! +

+ {/*
+ {( + [ + { label: 'React', value: 'react' }, + { label: 'Preact', value: 'preact' }, + { label: 'Solid', value: 'solid' }, + { label: 'Vue', value: 'vue' }, + { label: 'Svelte', value: 'svelte' }, + ] as const + ).map((item) => ( + + ))} +
*/} +
+
+ + {/* {['preact', 'vue', 'solid', 'svelte'].includes(framework) ? ( +
+
+ Looking for the @tanstack/{framework}-router{' '} + example? The @tanstack/{framework}-router adapter + is currently under development! Join the{' '} + + TanStack Discord Server + {' '} + and help us make routing in {framework} a better place! +
+
+ ) : ( */} +
+ +
+
+ +
+

+ Sponsors +

+
+ } + children={(sponsors) => { + return + }} + /> +
+ +
+ +
+

+ Partners +

+
+
+ + Router You? + +
+
+ We're looking for a TanStack Router OSS Partner to go above and + beyond the call of sponsorship. Are you as invested in TanStack + Router as we are? Let's push the boundaries of Router together! +
+ + Let's chat + +
+
+
+ +
+
+ Wow, you've come a long way! +
+
+ Only one thing left to do... +
+
+ + Get Started! + +
+
+
+
+ ) +} diff --git a/app/routes/_libraries.start.$version.index.tsx b/app/routes/_libraries.start.$version.index.tsx new file mode 100644 index 000000000..a0ce56c4e --- /dev/null +++ b/app/routes/_libraries.start.$version.index.tsx @@ -0,0 +1,529 @@ +import * as React from 'react' + +import { CgCornerUpLeft, CgSpinner } from 'react-icons/cg' +import { + FaBolt, + FaBook, + FaCheckCircle, + FaCogs, + FaDiscord, + FaGithub, + FaTshirt, + FaTwitter, +} from 'react-icons/fa' +import { Await, Link, getRouteApi } from '@tanstack/react-router' +import { Carbon } from '~/components/Carbon' +import { Footer } from '~/components/Footer' +import { VscPreview, VscWand } from 'react-icons/vsc' +import { TbHeartHandshake } from 'react-icons/tb' +import SponsorPack from '~/components/SponsorPack' +import { startProject } from '~/libraries/start' +import { createFileRoute } from '@tanstack/react-router' +import { Framework, getBranch } from '~/libraries' +import { seo } from '~/utils/seo' + +const menu = [ + { + label: ( +
+ TanStack +
+ ), + to: '/', + }, + // { + // label: ( + //
+ // Examples + //
+ // ), + // to: './docs/react/examples/basic', + // }, + // { + // label: ( + //
+ // Docs + //
+ // ), + // to: './docs/', + // }, + // { + // label: ( + //
+ // GitHub + //
+ // ), + // to: `https://github.com/${startProject.repo}`, + // }, + { + label: ( +
+ Discord +
+ ), + to: 'https://tlinz.com/discord', + }, + { + label: ( +
+ Merch +
+ ), + to: `https://cottonbureau.com/people/tanstack`, + }, +] + +export const Route = createFileRoute('/_libraries/start/$version/')({ + component: VersionIndex, + meta: () => + seo({ + title: startProject.name, + description: startProject.description, + }), +}) + +const librariesRouteApi = getRouteApi('/_libraries') + +export default function VersionIndex() { + const { sponsorsPromise } = librariesRouteApi.useLoaderData() + const { version } = Route.useParams() + const branch = getBranch(startProject, version) + const [framework, setFramework] = React.useState('react') + const [isDark, setIsDark] = React.useState(true) + + React.useEffect(() => { + if (isDark) { + // + } + setIsDark(window.matchMedia?.(`(prefers-color-scheme: dark)`).matches) + }, []) + + const gradientText = `inline-block text-transparent bg-clip-text bg-gradient-to-r ${startProject.colorFrom} ${startProject.colorTo}` + + return ( +
+
+ {menu?.map((item, i) => { + const label = ( +
{item.label}
+ ) + + return ( +
+ {item.to.startsWith('http') ? ( + {label} + ) : ( + + {label} + + )} +
+ ) + })} +
+
+
+

+ TanStack Start +

+
+ {/*
*/} +
+ Coming Soon! + {/* {version === 'latest' ? latestVersion : version} */} +
+ {/*
*/} +

+ Full-stack React framework{' '} + + powered by TanStack Router + {' '} +

+

+ Full-document SSR, Streaming, Server Functions, bundling and more, + powered by TanStack Router, Vinxi,{' '} + Nitro and Vite. Ready to deploy to + your favorite hosting provider. +

+
+
+
+ So when can I use it? +
+
+
+ TanStack Start is currently in development and is + not yet available for public use. We are working hard to bring you + the best possible experience and will be releasing more details + soon. In the meantime, you can follow along with the development + process by watching the commits on this very website! +
+
+ Yes, you heard that right!{' '} + + TanStack.com is already being built and deployed using TanStack + Start + + ! We are eating our own dog food and are excited to share the + results with you soon! +
+
+ +
+ {/*
+
+ +
+

+ Built on TanStack Router +

+

+ Writing your data fetching logic by hand is over. Tell TanStack + Query where to get your data and how fresh you need it to be and + the rest is automatic. It handles{' '} + + caching, background updates and stale data out of the box with + zero-configuration + + . +

+
+
+
+
+ +
+
+

+ Simple & Familiar +

+

+ If you know how to work with promises or async/await, then you + already know how to use TanStack Query. There's{' '} + + no global state to manage, reducers, normalization systems or + heavy configurations to understand + + . Simply pass a function that resolves your data (or throws an + error) and the rest is history. +

+
+
+
+
+ +
+
+

+ Extensible +

+

+ TanStack Query is configurable down to each observer instance of a + query with knobs and options to fit every use-case. It comes wired + up with{' '} + + dedicated devtools, infinite-loading APIs, and first class + mutation tools that make updating your data a breeze + + . Don't worry though, everything is pre-configured for success! +

+
+
+
*/} + + {/*
+
+

+ No dependencies. All the Features. +

+

+ With zero dependencies, TanStack Query is extremely lean given the + dense feature set it provides. From weekend hobbies all the way to + enterprise e-commerce systems (Yes, I'm lookin' at you Walmart! 😉), + TanStack Query is the battle-hardened tool to help you succeed at + the speed of your creativity. +

+
+
+ {[ + 'Backend agnostic', + 'Dedicated Devtools', + 'Auto Caching', + 'Auto Refetching', + 'Window Focus Refetching', + 'Polling/Realtime Queries', + 'Parallel Queries', + 'Dependent Queries', + 'Mutations API', + 'Automatic Garbage Collection', + 'Paginated/Cursor Queries', + 'Load-More/Infinite Scroll Queries', + 'Scroll Recovery', + 'Request Cancellation', + 'Suspense Ready!', + 'Render-as-you-fetch', + 'Prefetching', + 'Variable-length Parallel Queries', + 'Offline Support', + 'SSR Support', + 'Data Selectors', + ].map((d, i) => { + return ( + + {d} + + ) + })} +
+
*/} + + {/*
+
+ Trusted in Production by +
+ +
+ {(new Array(4) as string[]) + .fill('') + .reduce( + (all) => [...all, ...all], + [ + 'Google', + 'Walmart', + 'Facebook', + 'PayPal', + 'Amazon', + 'American Express', + 'Microsoft', + 'Target', + 'Ebay', + 'Autodesk', + 'CarFAX', + 'Docusign', + 'HP', + 'MLB', + 'Volvo', + 'Ocado', + 'UPC.ch', + 'EFI.com', + 'ReactBricks', + 'Nozzle.io', + 'Uber', + ] + ) + .map((d, i) => ( + + {d} + + ))} +
+
+
*/} + +
+

+ Partners +

+
+
+ + Start You? + +
+
+ We're looking for a TanStack Start OSS Partner to go above and + beyond the call of sponsorship. Are you as invested in TanStack + Start as we are? Let's push the boundaries of Start together! +
+ + Let's chat + +
+
+
+ +
+

+ Sponsors +

+
+ } + children={(sponsors) => { + return + }} + /> +
+ +
+ +
+
+ +
+ + This ad helps us be happy about our invested time and not burn out and + rage-quit OSS. Yay money! 😉 + +
+ + {/*
+
+

+ Less code, fewer edge cases. +

+

+ Instead of writing reducers, caching logic, timers, retry logic, + complex async/await scripting (I could keep going...), you literally + write a tiny fraction of the code you normally would. You will be + surprised at how little code you're writing or how much code you're + deleting when you use TanStack Query. Try it out with one of the + examples below! +

+
+ {( + [ + { label: 'Angular', value: 'angular' }, + { label: 'React', value: 'react' }, + { label: 'Solid', value: 'solid' }, + { label: 'Svelte', value: 'svelte' }, + { label: 'Vue', value: 'vue' }, + ] as const + ).map((item) => ( + + ))} +
+
+
*/} + + {/* {[''].includes(framework) ? ( +
+
+ Looking for the @tanstack/{framework}-query{' '} + example? We could use your help to build the{' '} + @tanstack/{framework}-query adapter! Join the{' '} + + TanStack Discord Server + {' '} + and let's get to work! +
+
+ ) : ( +
+ +
+ )} */} + + {/*
+
+ Wow, you've come a long way! +
+
+ Only one thing left to do... +
+
+ + Read the Docs! + +
+
*/} +
+
+ ) +} diff --git a/app/routes/_libraries.store.$version.index.tsx b/app/routes/_libraries.store.$version.index.tsx new file mode 100644 index 000000000..bfeb6d261 --- /dev/null +++ b/app/routes/_libraries.store.$version.index.tsx @@ -0,0 +1,424 @@ +import { CgCornerUpLeft, CgSpinner } from 'react-icons/cg' +import { FaBook, FaDiscord, FaGithub, FaTshirt } from 'react-icons/fa' +import { Link, createFileRoute, getRouteApi } from '@tanstack/react-router' +import { Carbon } from '~/components/Carbon' +import { Footer } from '~/components/Footer' +import { VscPreview } from 'react-icons/vsc' +import { TbHeartHandshake } from 'react-icons/tb' +import SponsorPack from '~/components/SponsorPack' +import { storeProject } from '~/libraries/store' +import { Await } from '@tanstack/react-router' +import { seo } from '~/utils/seo' + +const menu = [ + { + label: ( +
+ TanStack +
+ ), + to: '/', + }, + { + label: ( +
+ Examples +
+ ), + to: './docs/framework/react/examples/simple', + }, + { + label: ( +
+ Docs +
+ ), + to: './docs/', + }, + { + label: ( +
+ GitHub +
+ ), + to: `https://github.com/${storeProject.repo}`, + }, + { + label: ( +
+ Discord +
+ ), + to: 'https://tlinz.com/discord', + }, + { + label: ( +
+ Merch +
+ ), + to: `https://cottonbureau.com/people/tanstack`, + }, +] + +export const Route = createFileRoute('/_libraries/store/$version/')({ + component: StoreVersionIndex, + meta: () => + seo({ + title: storeProject.name, + description: storeProject.description, + }), +}) + +const librariesRouteApi = getRouteApi('/_libraries') + +export default function StoreVersionIndex() { + const { sponsorsPromise } = librariesRouteApi.useLoaderData() + const { version } = Route.useParams() + // const branch = getBranch(version) + // const [framework, setFramework] = React.useState('react') + // const [isDark, setIsDark] = React.useState(true) + + const gradientText = `inline-block text-transparent bg-clip-text bg-gradient-to-r ${storeProject.colorFrom} ${storeProject.colorTo}` + + return ( + <> +
+
+ {menu?.map((item, i) => { + const label = ( +
+ {item.label} +
+ ) + + return ( +
+ {item.to.startsWith('http') ? ( + {label} + ) : ( + + {label} + + )} +
+ ) + })} +
+
+
+

+ TanStack Store{' '} + + {version === 'latest' ? storeProject.latestVersion : version} + +

+
+

+ + Framework agnostic + {' '} + type-safe store w/ reactive framework adapters +

+

+ Level up your state management with TanStack Store – the + framework-agnostic, type-safe store. Enjoy{' '} + + minimal setup, granular APIs, and seamless adaptability across + frameworks + + . Simplify your development and boost efficiency with TanStack + Store. +

+ + Get Started + +
+ {/*
+
+ +
+

+ First-Class TypeScript Support +

+

+ TanStack Form touts first-class TypeScript support with + outstanding autocompletion, excellent generic throughput and + inferred types everywhere possible.{' '} + + This results in fewer runtime errors, increased code + maintainability, and a smoother development experience + {' '} + to help you confidently build robust and type-safe form + solutions that scale. +

+
+
+
+
+ +
+
+

+ Headless and Framework Agnostic +

+

+ Form's headless and framework agnostic approach ensures maximum + flexibility and broad compatibility with many front-end + frameworks, or no framework at all. By both supplying and + encouraging a headless approach to your forms, building custom + reusable form components tailored to your application's needs{' '} + + requires little abstraction and keeps your code modular, + simple and composable. + +

+
+
+
+
+ +
+
+

+ Granular Reactive Performance +

+

+ When it comes to performance, TanStack Form delivers amazing + speed and control, but without the cruft, boilerplate, or + abstractions. With granularly reactive APIs at its core,{' '} + + only relevant components are updated when the form state + changes. + {' '} + The end result? A faster UI, happy users, and zero worries about + performance. +

+
+
+
*/} + + {/*
+
+

+ No dependencies. All the Features. +

+

+ With zero dependencies, TanStack Form is extremely lean given the + dense feature set it provides. From weekend hobbies all the way to + enterprise TanStack Form has the tools to help you succeed at the + speed of your creativity. +

+
+
+ {[ + // A list of features that @tanstack/form provides for managing form state, validation, touched/dirty states, UI integration, etc. + 'Framework agnostic design', + 'First Class TypeScript Support', + 'Headless', + 'Tiny / Zero Deps', + 'Granularly Reactive Components/Hooks', + 'Extensibility and plugin architecture', + 'Modular architecture', + 'Form/Field validation', + 'Async Validation', + 'Built-in Async Validation Debouncing', + 'Configurable Validation Events', + 'Deeply Nested Object/Array Fields', + ].map((d, i) => { + return ( + + {d} + + ) + })} +
+
*/} + +
+

+ Partners +

+
+
+ + Store You? + +
+
+ We're looking for a TanStack Store OSS Partner to go above and + beyond the call of sponsorship. Are you as invested in TanStack + Store as we are? Let's push the boundaries of Store together! +
+ + Let's chat + +
+
+
+ +
+

+ Sponsors +

+
+ } + children={(sponsors) => { + return + }} + /> +
+ +
+ +
+
+ +
+ + This ad helps us be happy about our invested time and not burn out + and rage-quit OSS. Yay money! 😉 + +
+ + {/*
+
+

+ Less code, fewer edge cases. +

+

+ Instead of encouraging hasty abstractions and hook-focused APIs, + TanStack Form embraces composition where it counts by giving you + headless APIs via components (and hooks if you want them of + course). TanStack Form is designed to be used directly in your + components and UI. This means less code, fewer edge cases, and + deeper control over your UI. Try it out with one of the examples + below! +

+
+ {( + [ + { label: 'React', value: 'react' }, + { label: 'Solid', value: 'solid' }, + { label: 'Svelte', value: 'svelte' }, + { label: 'Vue', value: 'vue' }, + ] as const + ).map((item) => ( + + ))} +
+
+
+ + {['solid', 'vue', 'svelte'].includes(framework) ? ( +
+
+ Looking for the @tanstack/{framework}-form{' '} + example? We could use your help to build the{' '} + @tanstack/{framework}-form adapter! Join the{' '} + + TanStack Discord Server + {' '} + and let's get to work! +
+
+ ) : ( +
+ +
+ )} */} + +
+
+ Wow, you've come a long way! +
+
+ Only one thing left to do... +
+
+ + Get Started! + +
+
+
+
+ + ) +} diff --git a/app/routes/_libraries.table.$version.index.tsx b/app/routes/_libraries.table.$version.index.tsx new file mode 100644 index 000000000..69b7ae3d3 --- /dev/null +++ b/app/routes/_libraries.table.$version.index.tsx @@ -0,0 +1,481 @@ +import * as React from 'react' +import { CgCornerUpLeft, CgSpinner } from 'react-icons/cg' +import { + FaBolt, + FaBook, + FaCheckCircle, + FaCogs, + FaDiscord, + FaGithub, + FaTshirt, +} from 'react-icons/fa' +import { Link, createFileRoute, getRouteApi } from '@tanstack/react-router' +import { tableProject } from '~/libraries/table' +import { Carbon } from '~/components/Carbon' +import { Footer } from '~/components/Footer' +import { IoIosBody } from 'react-icons/io' +import SponsorPack from '~/components/SponsorPack' +import { VscPreview } from 'react-icons/vsc' +import agGridImage from '~/images/ag-grid.png' +import { Await } from '@tanstack/react-router' +import { Framework, getBranch } from '~/libraries' +import { seo } from '~/utils/seo' + +const menu = [ + { + label: ( +
+ TanStack +
+ ), + to: '/', + }, + { + label: ( +
+ Examples +
+ ), + to: './docs/framework/react/examples/basic', + }, + { + label: ( +
+ Docs +
+ ), + to: './docs/introduction', + }, + { + label: ( +
+ GitHub +
+ ), + to: `https://github.com/${tableProject.repo}`, + }, + { + label: ( +
+ Discord +
+ ), + to: 'https://tlinz.com/discord', + }, + { + label: ( +
+ Merch +
+ ), + to: `https://cottonbureau.com/people/tanstack`, + }, +] + +export const Route = createFileRoute('/_libraries/table/$version/')({ + component: TableVersionIndex, + meta: () => + seo({ + title: tableProject.name, + description: tableProject.description, + }), +}) + +const librariesRouteApi = getRouteApi('/_libraries') + +export default function TableVersionIndex() { + const { sponsorsPromise } = librariesRouteApi.useLoaderData() + const { version } = Route.useParams() + const branch = getBranch(tableProject, version) + const [framework, setFramework] = React.useState('react') + const [isDark, setIsDark] = React.useState(true) + + React.useEffect(() => { + setIsDark(window.matchMedia?.(`(prefers-color-scheme: dark)`).matches) + }, []) + + const gradientText = `inline-block text-transparent bg-clip-text bg-gradient-to-r ${tableProject.colorFrom} ${tableProject.colorTo}` + + return ( +
+
+ {menu?.map((item, i) => { + const label = ( +
{item.label}
+ ) + + return ( +
+ {item.to.startsWith('http') ? ( + {label} + ) : ( + + {label} + + )} +
+ ) + })} +
+
+
+

+ TanStack Table{' '} + + v8 + +

+
+

+ + Headless + {' '} + UI for building powerful tables & datagrids +

+

+ Supercharge your tables or build a datagrid from scratch for TS/JS, + React, Vue, Solid & Svelte while retaining 100% control over markup + and styles. +

+ + Get Started + +
+
+
+
+ +
+
+

+ Designed for zero design +

+

+ What good is a powerful table if that super hip designer you just + hired can't work their UI magic on it?{' '} + + TanStack Table is headless by design + + , which means 100% control down to the very smallest HTML tag, + component, class and style. Pixel Perfection? Go for it! +

+
+
+
+
+ +
+
+

+ Big Power, Small Package +

+

+ Don't be fooled by the small bundle size. TanStack Table is a + workhorse. It's built to materialize, filter, sort, group, + aggregate, paginate and display massive data sets using a very + small API surface. Wire up your new or existing tables and{' '} + + watch your users become instantly more productive + + . +

+
+
+
+
+ +
+
+

+ Extensible +

+

+ TanStack table ships with excellent defaults to get you off the + ground as fast as possible, but nothing is stopping you from{' '} + + customizing and overriding literally everything to your liking + + . Feeling tenacious enough to build your own Sheets/Excel/AirTable + clone? Be our guest 😉 +

+
+
+
+ +
+
+

+ Framework Agnostic & Feature Rich +

+

+ TanStack Table's API and engine are highly modular and + framework-independent while still prioritizing ergonomics. Behold, + the obligatory feature-list: +

+
+
+ {[ + 'Lightweight (10 - 15kb)', + 'Tree-Shaking', + 'Headless', + 'Cell Formatters', + 'Auto-managed internal state', + 'Opt-in fully controlled state', + 'Sorting', + 'Multi Sort', + 'Global Filters', + 'Columns Filters', + 'Pagination', + 'Row Grouping', + 'Aggregation', + 'Row Selection', + 'Row Expansion', + 'Column Ordering', + 'Column Visibility', + 'Column Resizing', + 'Virtualizable', + 'Server-side/external Data', + 'Nested/Grouped Headers', + 'Footers', + ].map((d, i) => { + return ( + + {d} + + ) + })} +
+
+ +
+
+ Trusted in Production by +
+ {/* @ts-ignore */} + +
+ {(new Array(4) as string[]) + .fill('') + .reduce( + (all) => [...all, ...all], + [ + 'Intuit', + 'Google', + 'Amazon', + 'Apple', + 'AutoZone', + 'Microsoft', + 'Cisco', + 'Uber', + 'Salesforce', + 'Walmart', + 'Wix', + 'HP', + 'Docusign', + 'Tripwire', + 'Yahoo!', + 'Ocado', + 'Nordstrom', + 'TicketMaster', + 'Comcast Business', + 'Nozzle.io', + ] + ) + .map((d, i) => ( + + {d} + + ))} +
+ {/* @ts-ignore */} +
+
+ +
+

+ Partners +

+
+
+ + AG Grid + +
+
+ TanStack Table and AG Grid are respectfully the{' '} + best table/datagrid libraries around. Instead of + competing, we're working together to ensure the highest quality + table/datagrid options are available for the entire JS/TS + ecosystem and every use-case. +
+ + Read More + +
+
+
+ +
+

+ Sponsors +

+
+ } + children={(sponsors) => { + return + }} + /> +
+ +
+ +
+
+ +
+ + This ad helps us be happy about our invested time and not burn out and + rage-quit OSS. Yay money! 😉 + +
+ +
+
+

+ Take it for a spin! +

+

+ With some basic styles, some table markup and few columns, you're + already well on your way to creating a drop-dead powerful table. +

+
+ {( + [ + { label: 'Angular', value: 'angular' }, + { label: 'Qwik', value: 'qwik' }, + { label: 'React', value: 'react' }, + { label: 'Solid', value: 'solid' }, + { label: 'Svelte', value: 'svelte' }, + { label: 'Vue', value: 'vue' }, + { label: 'Vanilla', value: 'vanilla' }, + ] as const + ).map((item) => ( + + ))} +
+
+
+ +
+ +
+ +
+
+ Wow, you've come a long way! +
+
+ Only one thing left to do... +
+
+ + Get Started! + +
+
+
+
+ ) +} diff --git a/app/routes/_libraries.tsx b/app/routes/_libraries.tsx new file mode 100644 index 000000000..401ad83be --- /dev/null +++ b/app/routes/_libraries.tsx @@ -0,0 +1,210 @@ +import * as React from 'react' +import { + Link, + Outlet, + createFileRoute, + defer, + useParams, +} from '@tanstack/react-router' +import { CgClose, CgMenuLeft } from 'react-icons/cg' +import { twMerge } from 'tailwind-merge' +import { sortBy } from '~/utils/utils' +import logoColor100w from '~/images/logo-color-100w.png' +import { FaInstagram, FaTshirt, FaTwitter } from 'react-icons/fa' +import { getSponsorsForSponsorPack } from '~/server/sponsors' +import { getLibrary, libraries } from '~/libraries' +import { Scarf } from '~/components/Scarf' + +export const Route = createFileRoute('/_libraries')({ + loader: async (ctx) => { + return { + sponsorsPromise: defer(getSponsorsForSponsorPack()), + } + }, + component: LibrariesLayout, +}) + +function LibrariesLayout() { + const { libraryId } = useParams({ + strict: false, + experimental_returnIntersection: true, + }) + const library = libraryId ? getLibrary(libraryId) : undefined + const detailsRef = React.useRef(null!) + + const items = ( + <> + {sortBy(libraries, (d) => !d.name.includes('TanStack')).map( + (project, i) => { + const linkClasses = twMerge( + `flex items-center justify-between group px-2 py-1 rounded-lg hover:bg-gray-500 hover:bg-opacity-10 font-black` + ) + + const [prefix, name] = project.name.split(' ') + + return ( +
+ {project.to.startsWith('http') ? ( + + + + {prefix} + {' '} + {name} + + + ) : ( + { + detailsRef.current.removeAttribute('open') + }} + // activeOptions={{ + // exact: true, + // }} + > + {(props) => { + return ( +
+ + + {prefix} + {' '} + {name} + + {project.badge ? ( + + {project.badge} + + ) : null} +
+ ) + }} + + )} +
+ ) + } + )} + + + Merch! + + New + + + + ) + + const logo = ( +
+ + +
TanStack
+ + +
+ ) + + const smallMenu = ( +
+
+ +
+ + + {logo} +
+
+
+
+ {items} +
+
+
+
+ ) + + const largeMenu = ( + <> +
+
+ {logo} +
+ +
+
+ {items} +
+
+
+ + ) + + return ( +
+ {smallMenu} + {largeMenu} +
+ {library?.scarfId ? : null} + +
+
+ ) +} diff --git a/app/routes/_libraries.virtual.$version.index.tsx b/app/routes/_libraries.virtual.$version.index.tsx new file mode 100644 index 000000000..f3f19dc78 --- /dev/null +++ b/app/routes/_libraries.virtual.$version.index.tsx @@ -0,0 +1,474 @@ +import * as React from 'react' + +import { CgCornerUpLeft, CgSpinner } from 'react-icons/cg' +import { + FaBolt, + FaBook, + FaCheckCircle, + FaCogs, + FaDiscord, + FaGithub, + FaTshirt, +} from 'react-icons/fa' +import { + Await, + Link, + createFileRoute, + getRouteApi, +} from '@tanstack/react-router' +import { virtualProject } from '~/libraries/virtual' +import { Carbon } from '~/components/Carbon' +import { Footer } from '~/components/Footer' +import { IoIosBody } from 'react-icons/io' +import SponsorPack from '~/components/SponsorPack' +import { TbHeartHandshake } from 'react-icons/tb' +import { VscPreview } from 'react-icons/vsc' +import { Logo } from '~/components/Logo' +import { getSponsorsForSponsorPack } from '~/server/sponsors' +import { Framework, getBranch } from '~/libraries' +import { seo } from '~/utils/seo' + +const menu = [ + { + label: ( +
+ TanStack +
+ ), + to: '/', + }, + { + label: ( +
+ Examples +
+ ), + to: './docs/framework/react/examples/dynamic', + }, + { + label: ( +
+ Docs +
+ ), + to: './docs/introduction', + }, + { + label: ( +
+ GitHub +
+ ), + to: `https://github.com/${virtualProject.repo}`, + }, + { + label: ( +
+ Discord +
+ ), + to: 'https://tlinz.com/discord', + }, + { + label: ( +
+ Merch +
+ ), + to: `https://cottonbureau.com/people/tanstack`, + }, +] + +export const Route = createFileRoute('/_libraries/virtual/$version/')({ + component: RouteComp, + meta: () => + seo({ + title: virtualProject.name, + description: virtualProject.description, + }), +}) + +const librariesRouteApi = getRouteApi('/_libraries') + +export default function RouteComp() { + const { sponsorsPromise } = librariesRouteApi.useLoaderData() + const { version } = Route.useParams() + const [framework, setFramework] = React.useState('react') + const branch = getBranch(virtualProject, version) + const [isDark, setIsDark] = React.useState(true) + + React.useEffect(() => { + setIsDark(window.matchMedia?.(`(prefers-color-scheme: dark)`).matches) + }, []) + + const gradientText = `inline-block text-transparent bg-clip-text bg-gradient-to-r ${virtualProject.colorFrom} ${virtualProject.colorTo}` + + return ( +
+
+ {menu?.map((item, i) => { + const label = ( +
{item.label}
+ ) + + return ( +
+ {item.to.startsWith('http') ? ( + {label} + ) : ( + + {label} + + )} +
+ ) + })} +
+
+
+

+ TanStack Virtual{' '} + + v3 + +

+
+

+ + Headless + {' '} + UI for Virtualizing Large Element Lists +

+

+ Virtualize only the visible DOM nodes within massive scrollable + elements at 60FPS in TS/JS, React, Vue, Solid & Svelte while retaining + 100% control over markup and styles. +

+ + Get Started + +
+
+
+
+ +
+
+

+ Designed for zero design +

+

+ Headless Virtualization means you're always in control of your{' '} + + markup, styles and components + + . Go design and implement the most beautiful UI you can dream up + and let us take care of the hard parts. +

+
+
+
+
+ +
+
+

+ Big Power, Small Package +

+

+ Don't be fooled by the small bundle size. TanStack Virtual uses + every byte to deliver powerful performance. After all,{' '} + + {' '} + 60FPS is table stakes + {' '} + these days and we refuse to sacrifice anything for that 🧈-y + smooth UX. +

+
+
+
+
+ +
+
+

+ Maximum Composability +

+

+ With a single function/hook, you'll get limitless virtualization + for{' '} + + vertical, horizontal, and grid-style{' '} + + layouts. The API is tiny (literally 1 function), but its + composability is not. +

+
+
+
+ +
+
+

+ Framework Agnostic & Feature Rich +

+

+ TanStack Table's API and engine are highly modular and + framework-independent while still prioritizing ergonomics. Behold, + the obligatory feature-list: +

+
+
+ {[ + 'Lightweight (10 - 15kb)', + 'Tree-Shaking', + 'Headless', + 'Vertical/Column Virtualization', + 'Horizontal/Row Virtualization', + 'Grid Virtualization', + 'Window-Scrolling', + 'Fixed Sizing', + 'Variable Sizing', + 'Dynamic/Measured Sizing', + 'Scrolling Utilities', + 'Sticky Items', + ].map((d, i) => { + return ( + + {d} + + ) + })} +
+
+ + {/*
+
+ Trusted in Production by +
+ +
+ {(new Array(4) as string[]) + .fill('') + .reduce( + (all) => [...all, ...all], + [ + 'Intuit', + 'Google', + 'Amazon', + 'Apple', + 'AutoZone', + 'Microsoft', + 'Cisco', + 'Uber', + 'Salesforce', + 'Walmart', + 'Wix', + 'HP', + 'Docusign', + 'Tripwire', + 'Yahoo!', + 'Ocado', + 'Nordstrom', + 'TicketMaster', + 'Comcast Business', + 'Nozzle.io', + ] + ) + .map((d, i) => ( + + {d} + + ))} +
+
+
*/} + +
+

+ Partners +

+
+
+ + Virtual You? + +
+
+ We're looking for a TanStack Virtual OSS Partner to go above and + beyond the call of sponsorship. Are you as invested in TanStack + Virtual as we are? Let's push the boundaries of Virtual together! +
+ + Let's chat + +
+
+
+ +
+

+ Sponsors +

+
+ } + children={(sponsors) => { + return + }} + /> +
+ +
+ +
+
+ +
+ + This ad helps us be happy about our invested time and not burn out and + rage-quit OSS. Yay money! 😉 + +
+ +
+
+

+ Take it for a spin! +

+

+ With just a few divs and some inline styles, you're already well on + your way to creating an extremely powerful virtualization + experience. +

+
+ {( + [ + { label: 'React', value: 'react' }, + { label: 'Solid', value: 'solid' }, + { label: 'Svelte', value: 'svelte' }, + { label: 'Vue', value: 'vue' }, + ] as const + ).map((item) => ( + + ))} +
+
+
+ + {['vue', 'solid', 'svelte'].includes(framework) ? ( +
+
+ Looking for the @tanstack/{framework}-virtual{' '} + example? We could use your help to build the{' '} + @tanstack/{framework}-virtual adapter! Join the{' '} + + TanStack Discord Server + {' '} + and let's get to work! +
+
+ ) : ( +
+ +
+ )} + +
+
+ Wow, you've come a long way! +
+
+ Only one thing left to do... +
+
+ + Get Started! + +
+
+
+
+ ) +} diff --git a/app/routes/blog.$.tsx b/app/routes/blog.$.tsx new file mode 100644 index 000000000..4b76e1d1d --- /dev/null +++ b/app/routes/blog.$.tsx @@ -0,0 +1,62 @@ +import { createFileRoute, notFound } from '@tanstack/react-router' +import { extractFrontMatter, fetchRepoFile } from '~/utils/documents.server' +import removeMarkdown from 'remove-markdown' +import { seo } from '~/utils/seo' +import { Doc } from '~/components/Doc' +import { PostNotFound } from './blog' +import { createServerFn } from '@tanstack/react-router-server' + +const fetchBlogPost = createServerFn( + 'GET', + async ({ docsPath }: { docsPath: string }, req) => { + 'use server' + + if (!docsPath) { + throw new Error('Invalid docs path') + } + + const filePath = `app/blog/${docsPath}.md` + + const file = await fetchRepoFile('tanstack/tanstack.com', 'main', filePath) + + if (!file) { + throw notFound() + } + + const frontMatter = extractFrontMatter(file) + const description = removeMarkdown(frontMatter.excerpt ?? '') + + return { + title: frontMatter.data.title, + description, + published: frontMatter.data.published, + content: frontMatter.content, + filePath, + } + } +) + +export const Route = createFileRoute('/blog/$')({ + loader: ({ params }) => fetchBlogPost({ docsPath: params._splat }), + meta: ({ loaderData }) => + seo({ + title: `${loaderData?.title ?? 'Docs'} | TanStack Blog`, + description: loaderData?.description, + }), + notFoundComponent: () => , + component: BlogPost, +}) + +export default function BlogPost() { + const { title, content, filePath } = Route.useLoaderData() + + return ( + + ) +} diff --git a/app/routes/blog.index.tsx b/app/routes/blog.index.tsx new file mode 100644 index 000000000..679e61870 --- /dev/null +++ b/app/routes/blog.index.tsx @@ -0,0 +1,120 @@ +import { Link, createFileRoute, notFound } from '@tanstack/react-router' + +import { getPostList } from '~/utils/blog' +import { DocTitle } from '~/components/DocTitle' +import { RenderMarkdown } from '~/components/RenderMarkdown' +import { format } from 'date-fns' +import { Footer } from '~/components/Footer' +import { extractFrontMatter, fetchRepoFile } from '~/utils/documents.server' +import { PostNotFound } from './blog' +import { createServerFn } from '@tanstack/react-router-server' + +const fetchFrontMatters = createServerFn('GET', async () => { + 'use server' + + const postInfos = getPostList() + + const frontMatters = await Promise.all( + postInfos.map(async (info) => { + const filePath = `app/blog/${info.id}.md` + + const file = await fetchRepoFile( + 'tanstack/tanstack.com', + 'main', + filePath + ) + + if (!file) { + throw notFound() + } + + const frontMatter = extractFrontMatter(file) + + return [ + info.id, + { + title: frontMatter.data.title, + published: frontMatter.data.published, + excerpt: frontMatter.excerpt, + }, + ] as const + }) + ) + + return frontMatters + + // return json(frontMatters, { + // headers: { + // 'Cache-Control': 'public, max-age=300, s-maxage=3600', + // }, + // }) +}) + +export const Route = createFileRoute('/blog/')({ + loader: () => fetchFrontMatters(), + notFoundComponent: () => , + component: BlogIndex, + meta: () => [ + { + title: 'Blog', + }, + ], +}) + +function BlogIndex() { + const frontMatters = Route.useLoaderData() + + return ( +
+
+
+ Latest Posts +
+
+
+
+
+ {frontMatters.map(([id, { title, published, excerpt }]) => { + return ( + +
+
{title}
+ {published ? ( +
+ {format(new Date(published), 'MMM dd, yyyy')} +
+ ) : null} +
+ , + }} + > + {excerpt || ''} + +
+
+
+
+ Read More +
+
+ + ) + })} +
+
+
+
+
+ ) +} diff --git a/app/routes/blog.tsx b/app/routes/blog.tsx new file mode 100644 index 000000000..7acbf7bee --- /dev/null +++ b/app/routes/blog.tsx @@ -0,0 +1,155 @@ +import * as React from 'react' +import { + DefaultGlobalNotFound, + Link, + Outlet, + createFileRoute, +} from '@tanstack/react-router' +import { Carbon } from '~/components/Carbon' +import { seo } from '~/utils/seo' +import { CgClose, CgMenuLeft } from 'react-icons/cg' + +const logo = ( + <> + + TanStack + + + + Blog + + + +) + +const localMenu = [ + { + label: 'Blog', + children: [ + { + label: 'Latest Posts', + to: '.', + }, + ], + }, +] as const + +export const Route = createFileRoute('/blog')({ + meta: () => + seo({ + title: 'Blog | TanStack', + description: 'The latest news and blog posts from TanStack!', + }), + component: Blog, +}) + +export function PostNotFound() { + return ( +
+

+
404
+
Not Found
+

+
Post not found.
+ + Blog Home + +
+ ) +} + +function Blog() { + const detailsRef = React.useRef(null!) + + const menuItems = localMenu.map((group) => { + return ( +
+
{group.label}
+
+
+ {group.children?.map((child, i) => { + return ( +
+ {child.to.startsWith('http') ? ( + {child.label} + ) : ( + { + detailsRef.current.removeAttribute('open') + }} + activeOptions={{ + exact: true, + }} + > + {child.label} + + )} +
+ ) + })} +
+
+ ) + }) + + const smallMenu = ( +
+
+ +
+ + + {logo} +
+
+
+ {menuItems} +
+
+
+ ) + + const largeMenu = ( +
+
{logo}
+
+
+ {menuItems} +
+
+ +
+
+ ) + + return ( +
+ {smallMenu} + {largeMenu} +
+ +
+
+ ) +} diff --git a/app/routes/dashboard.tsx b/app/routes/dashboard.tsx new file mode 100644 index 000000000..75fcd8872 --- /dev/null +++ b/app/routes/dashboard.tsx @@ -0,0 +1,59 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/react-router-server' +import { redirectWithClearedCookie, requireAuthCookie } from '~/auth/auth' +import { useMutation } from '~/hooks/useMutation' + +const loadDashboard = createServerFn('GET', async (_, { request }) => { + 'use server' + + const userId = await requireAuthCookie(request) + + return { + userId, + } +}) + +const logoutFn = createServerFn('POST', async () => { + 'use server' + + return redirectWithClearedCookie() +}) + +export const Route = createFileRoute('/dashboard')({ + loader: () => loadDashboard(), + component: LoginComp, +}) + +function LoginComp() { + const { userId } = Route.useLoaderData() + + const mutation = useMutation({ + fn: logoutFn, + }) + + return ( +
+

Dashboard

+
+ Welcome! Your userId is "{userId}" This is an experiment to test: +
+
    +
  • + Our ability to access original document request headers and cookies in + server functions +
  • +
  • Our ability to clear cookies in server functions
  • +
  • Our ability to redirect from server functions
  • +
  • Our ability to use a custom hook to manage mutations
  • +
+
+ +
+
+ ) +} diff --git a/app/routes/index.tsx b/app/routes/index.tsx new file mode 100644 index 000000000..af03a6682 --- /dev/null +++ b/app/routes/index.tsx @@ -0,0 +1,561 @@ +import { Await, Link, createFileRoute, defer } from '@tanstack/react-router' +import { Carbon } from '~/components/Carbon' +import { twMerge } from 'tailwind-merge' +import { FaDiscord, FaGithub, FaTshirt } from 'react-icons/fa' +import { CgMusicSpeaker, CgSpinner } from 'react-icons/cg' +import { Footer } from '~/components/Footer' +import SponsorPack from '~/components/SponsorPack' +import { LogoColor } from '~/components/LogoColor' +import { getSponsorsForSponsorPack } from '~/server/sponsors' +import discordImage from '~/images/discord-logo-white.svg' +import agGridImage from '~/images/ag-grid.png' +import nozzleImage from '~/images/nozzle.png' +import bytesImage from '~/images/bytes.svg' +import bytesUidotdevImage from '~/images/bytes-uidotdev.png' +import { useMutation } from '~/hooks/useMutation' +import { sample } from '~/utils/utils' +import { libraries } from '~/libraries' + +export const textColors = [ + `text-rose-500`, + `text-yellow-500`, + `text-teal-500`, + `text-blue-500`, +] + +export const gradients = [ + `from-rose-500 to-yellow-500`, + `from-yellow-500 to-teal-500`, + `from-teal-500 to-violet-500`, + `from-blue-500 to-pink-500`, +] + +const courses = [ + { + name: 'The Official TanStack React Query Course', + cardStyles: `border-t-4 border-red-500 hover:(border-green-500)`, + href: 'https://query.gg/?s=tanstack', + description: `Learn how to build enterprise quality apps with TanStack's React Query the easy way with our brand new course.`, + }, +] + +export const Route = createFileRoute('/')({ + loader: () => { + return { + randomNumber: Math.random(), + sponsorsPromise: defer(getSponsorsForSponsorPack()), + } + }, + component: Index, +}) + +async function bytesSignupServerFn({ email }: { email: string }) { + 'use server' + + return fetch(`https://bytes.dev/api/bytes-optin-cors`, { + method: 'POST', + body: JSON.stringify({ + email, + influencer: 'tanstack', + }), + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }) +} + +function Index() { + const bytesSignupMutation = useMutation({ + fn: bytesSignupServerFn, + }) + + const { sponsorsPromise, randomNumber } = Route.useLoaderData() + const gradient = sample(gradients, randomNumber) + const textColor = sample(textColors, randomNumber) + + return ( + <> +
+ {[ + { + label: ( +
+ + Merch +
+ ), + to: 'https://cottonbureau.com/people/tanstack', + }, + { + label: ( +
+ Blog +
+ ), + to: '/blog', + }, + { + label: ( +
+ GitHub +
+ ), + to: 'https://github.com/tanstack', + }, + { + label: ( +
+ Discord +
+ ), + to: 'https://tlinz.com/discord', + }, + ]?.map((item, i) => { + const label = ( +
{item.label}
+ ) + + return ( +
+ {item.to.startsWith('http') ? ( + + {label} + + ) : ( + + {label} + + )} +
+ ) + })} +
+
+
+ +

+ + TanStack + +

+
+

+ High-quality open-source software for{' '} + + web developers. + +

+

+ Headless, type-safe, & powerful utilities for State Management, + Routing, Data Visualization, Charts, Tables, and more. +

+
+
+
+

Open Source Libraries

+
+ {libraries.map((project, i) => { + return ( + +
+
+ {project.name} +
+ {project.badge ? ( +
+ {project.badge} +
+ ) : null} +
+
+ {project.tagline} +
+
+ {project.description} +
+ + ) + })} +
+
+
+
+

Partners

+
+
+
+ + Enterprise Data Grid + +
+
+
+ TanStack Table and AG Grid are respectfully the{' '} + best table/datagrid libraries around. Instead + of competing, we're working together to ensure the highest + quality table/datagrid options are available for the entire + JS/TS ecosystem and every use-case imaginable for UI/UX + developers. +
+ + Learn More + +
+
+
+
+ + Bytes Logo + +
+
+
+ TanStack's priority is to make its users productive, efficient + and knowledgeable about web dev. To help us on this quest, we've + partnered with{' '} + + ui.dev + {' '} + to provide best-in-class education about + TanStack products. It doesn't stop at TanStack though, with + their sister product{' '} + + Bytes.dev + {' '} + as our official newsletter partner, you'll be able to{' '} + stay up to date with the latest and greatest in + the web dev world regardless. +
+ + Learn More + +
+
+
+
+ + SEO keyword rank tracker + +
+
+
+ Since its founding, Nozzle's SEO platform has used TanStack + libraries to build one of the{' '} + + most technically advanced search engine monitoring platforms + + , its enterprise rank tracking and keyword research tools are + setting a new bar for quality and scale. Nozzle uses the full + gamut of TanStack tools on the front-end to deliver users with + unmatched UI/UX. +
+ + Learn More + +
+
+
+
+
+
+
+
+ +
+ + This ad helps us be happy about our invested time and not burn out + and rage-quit OSS. Yay money! 😉 + +
+
+
+
+

Courses

+ +
+
+
+

OSS Sponsors

+
+
+ } + children={(sponsors) => { + return + }} + /> +
+
+
+ +
+

+ Sponsors get special perks like{' '} + + private discord channels, priority issue requests, direct support + and even course vouchers + + ! +

+
+
+
+
+
+
+ +
+
+

TanStack on Discord

+

+ The official TanStack community to ask questions, network and make + new friends and get lightning fast news about what's coming next + for TanStack! +

+
+ +
+
+
+
+
+ {!bytesSignupMutation.submittedAt ? ( +
{ + e.preventDefault() + const formData = new FormData(e.currentTarget) + + bytesSignupMutation.mutate({ + email: formData.get('email_address')?.toString() || '', + }) + }} + > +
+
+

Subscribe to Bytes

+
+ Bytes Logo +
+
+ +

+ The Best JavaScript Newsletter +

+
+
+ + +
+ {bytesSignupMutation.error ? ( +

+ Looks like something went wrong. Please try again. +

+ ) : ( +

+ Join over 100,000 devs +

+ )} +
+ ) : ( +

🎉 Thank you! Please confirm your email

+ )} +
+
+
+