Documentation
Framework
Version

Solid Example: Kitchen Sink

tsx
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import { render } from 'solid-js/web'
import {
  ErrorComponent,
  Link,
  MatchRoute,
  Outlet,
  RouterProvider,
  createRootRouteWithContext,
  createRoute,
  createRouter,
  lazyRouteComponent,
  redirect,
  retainSearchParams,
  useNavigate,
  useRouter,
  useRouterState,
  useSearch,
} from '@tanstack/solid-router'
import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools'
import {
  createEffect,
  createMemo,
  createRenderEffect,
  createSignal,
  on,
} from 'solid-js'
import { z } from 'zod'
import {
  fetchInvoiceById,
  fetchInvoices,
  fetchUserById,
  fetchUsers,
  patchInvoice,
  postInvoice,
} from './mockTodos'
import { useMutation } from './useMutation'
import type { Invoice } from './mockTodos'
import './styles.css'

//

type UsersViewSortBy = 'name' | 'id' | 'email'

const rootRoute = createRootRouteWithContext<{
  auth: Auth
}>()({
  component: RootComponent,
})

function RouterSpinner() {
  const isLoading = useRouterState({ select: (s) => s.status === 'pending' })
  return <Spinner show={isLoading()} />
}

function RootComponent() {
  return (
    <>
      <div class={`min-h-screen flex flex-col`}>
        <div class={`flex items-center border-b gap-2`}>
          <h1 class={`text-3xl p-2`}>Kitchen Sink</h1>
          {/* Show a global spinner when the router is transitioning */}
          <div class={`text-3xl`}>
            <RouterSpinner />
          </div>
        </div>
        <div class={`flex-1 flex`}>
          <div class={`divide-y w-56`}>
            {(
              [
                ['/', 'Home'],
                ['/dashboard', 'Dashboard'],
                ['/expensive', 'Expensive'],
                ['/route-a', 'Pathless Layout A'],
                ['/route-b', 'Pathless Layout B'],
                ['/profile', 'Profile'],
                ...(auth.status === 'loggedOut' ? [['/login', 'Login']] : []),
              ] as const
            ).map(([to, label]) => {
              return (
                <div>
                  <Link
                    to={to}
                    activeOptions={
                      {
                        // If the route points to the root of it's parent,
                        // make sure it's only active if it's exact
                        // exact: to === '.',
                      }
                    }
                    preload="intent"
                    class={`block py-2 px-3 text-blue-700`}
                    // Make "active" links bold
                    activeProps={{ class: `font-bold` }}
                  >
                    {label}
                  </Link>
                </div>
              )
            })}
          </div>
          <div class={`flex-1 border-l`}>
            {/* Render our first route match */}
            <Outlet />
          </div>
        </div>
      </div>
      <TanStackRouterDevtools position="bottom-right" />
    </>
  )
}

const indexRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/',
  component: IndexComponent,
})

function IndexComponent() {
  return (
    <div class={`p-2`}>
      <div class={`text-lg`}>Welcome Home!</div>
      <hr class={`my-2`} />
      <Link
        to={invoiceRoute.to}
        params={{
          invoiceId: 3,
        }}
        class={`py-1 px-2 text-xs bg-blue-500 text-white rounded-full`}
      >
        1 New Invoice
      </Link>
      <hr class={`my-2`} />
      <div class={`max-w-xl`}>
        As you navigate around take note of the UX. It should feel
        suspense-like, where routes are only rendered once all of their data and
        elements are ready.
        <hr class={`my-2`} />
        To exaggerate async effects, play with the artificial request delay
        slider in the bottom-left corner.
        <hr class={`my-2`} />
        The last 2 sliders determine if link-hover preloading is enabled (and
        how long those preloads stick around) and also whether to cache rendered
        route data (and for how long). Both of these default to 0 (or off).
      </div>
    </div>
  )
}

const dashboardLayoutRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'dashboard',
  component: DashboardLayoutComponent,
})

function DashboardLayoutComponent() {
  return (
    <>
      <div class="flex items-center border-b">
        <h2 class="text-xl p-2">Dashboard</h2>
      </div>
      <div class="flex flex-wrap divide-x">
        {(
          [
            ['/dashboard', 'Summary', true],
            ['/dashboard/invoices', 'Invoices'],
            ['/dashboard/users', 'Users'],
          ] as const
        ).map(([to, label, exact]) => {
          return (
            <Link
              to={to}
              activeOptions={{ exact }}
              activeProps={{ class: `font-bold` }}
              class="p-2"
            >
              {label}
            </Link>
          )
        })}
      </div>
      <hr />
      <Outlet />
    </>
  )
}

const dashboardIndexRoute = createRoute({
  getParentRoute: () => dashboardLayoutRoute,
  path: '/',
  loader: () => fetchInvoices(),
  component: DashboardIndexComponent,
})

function DashboardIndexComponent() {
  const invoices = dashboardIndexRoute.useLoaderData()

  return (
    <div class="p-2">
      <div class="p-2">
        Welcome to the dashboard! You have{' '}
        <strong>{invoices.length} total invoices</strong>.
      </div>
    </div>
  )
}

const invoicesLayoutRoute = createRoute({
  getParentRoute: () => dashboardLayoutRoute,
  path: 'invoices',
  loader: () => fetchInvoices(),
  component: InvoicesLayoutComponent,
})

function InvoicesLayoutComponent() {
  const invoices = invoicesLayoutRoute.useLoaderData()

  return (
    <div class="flex-1 flex">
      <div class="divide-y w-48">
        {invoices().map((invoice) => {
          return (
            <div>
              <Link
                to="/dashboard/invoices/$invoiceId"
                params={{
                  invoiceId: invoice.id,
                }}
                preload="intent"
                class="block py-2 px-3 text-blue-700"
                activeProps={{ class: `font-bold ` }}
              >
                <pre class="text-sm">
                  #{invoice.id} - {invoice.title.slice(0, 10)}{' '}
                  {/* {updateSubmission ? (
                      <Spinner />
                    ) : ( */}
                  <MatchRoute
                    to={invoiceRoute.to}
                    params={{
                      invoiceId: invoice.id,
                    }}
                    pending
                  >
                    {(match) => <Spinner show={!!match} wait="delay-50" />}
                  </MatchRoute>
                  {/* )} */}
                </pre>
              </Link>
            </div>
          )
        })}
      </div>
      <div class="flex-1 border-l">
        <Outlet />
      </div>
    </div>
  )
}

const invoicesIndexRoute = createRoute({
  getParentRoute: () => invoicesLayoutRoute,
  path: '/',
  component: InvoicesIndexComponent,
})

function InvoicesIndexComponent() {
  const createInvoiceMutation = useMutation({
    fn: postInvoice,
    onSuccess: () => router.invalidate(),
  })

  return (
    <>
      <div class="p-2">
        <form
          onSubmit={(event) => {
            event.preventDefault()
            event.stopPropagation()
            const formData = new FormData(event.target as HTMLFormElement)
            createInvoiceMutation.mutate({
              title: formData.get('title') as string,
              body: formData.get('body') as string,
            })
          }}
          class="space-y-2"
        >
          <div>Create a new Invoice:</div>
          <InvoiceFields invoice={{} as Invoice} />
          <div>
            <button
              class="bg-blue-500 rounded-sm p-2 uppercase text-white font-black disabled:opacity-50"
              disabled={createInvoiceMutation.status === 'pending'}
            >
              {createInvoiceMutation.status === 'pending' ? (
                <>
                  Creating <Spinner />
                </>
              ) : (
                'Create'
              )}
            </button>
          </div>
          {createInvoiceMutation.status === 'success' ? (
            <div class="inline-block px-2 py-1 rounded-sm bg-green-500 text-white animate-bounce [animation-iteration-count:2.5] [animation-duration:.3s]">
              Created!
            </div>
          ) : createInvoiceMutation.status === 'error' ? (
            <div class="inline-block px-2 py-1 rounded-sm bg-red-500 text-white animate-bounce [animation-iteration-count:2.5] [animation-duration:.3s]">
              Failed to create.
            </div>
          ) : null}
        </form>
      </div>
    </>
  )
}

const invoiceRoute = createRoute({
  getParentRoute: () => invoicesLayoutRoute,
  path: '$invoiceId',
  params: {
    parse: (params) => ({
      invoiceId: z.number().int().parse(Number(params.invoiceId)),
    }),
    stringify: ({ invoiceId }) => ({ invoiceId: `${invoiceId}` }),
  },
  validateSearch: (search) =>
    z
      .object({
        showNotes: z.boolean().optional(),
        notes: z.string().optional(),
      })
      .parse(search),
  loader: ({ params: { invoiceId } }) => fetchInvoiceById(invoiceId),
  component: InvoiceComponent,
  pendingComponent: () => <Spinner />,
})

function InvoiceComponent() {
  const search = invoiceRoute.useSearch()
  const navigate = useNavigate({ from: invoiceRoute.fullPath })
  const invoice = invoiceRoute.useLoaderData()
  const updateInvoiceMutation = useMutation({
    fn: patchInvoice,
    onSuccess: () => router.invalidate(),
  })
  const [notes, setNotes] = createSignal(search().notes ?? '')
  createEffect(
    on(notes, () => {
      navigate({
        search: (old) => ({
          ...old,
          notes: notes() ? notes() : undefined,
        }),
        params: true,
        replace: true,
      })
    }),
  )

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault()
        event.stopPropagation()
        const formData = new FormData(event.target as HTMLFormElement)
        updateInvoiceMutation.mutate({
          id: invoice().id,
          title: formData.get('title') as string,
          body: formData.get('body') as string,
        })
      }}
      class="p-2 space-y-2"
    >
      <InvoiceFields
        invoice={invoice()}
        disabled={updateInvoiceMutation.status === 'pending'}
      />
      <div>
        <Link
          search={(old) => ({
            ...old,
            showNotes: old.showNotes ? undefined : true,
          })}
          class="text-blue-700"
          from={invoiceRoute.fullPath}
          params={true}
        >
          {search().showNotes ? 'Close Notes' : 'Show Notes'}{' '}
        </Link>
        {search().showNotes ? (
          <>
            <div>
              <div class="h-2" />
              <textarea
                value={notes()}
                onChange={(e) => {
                  setNotes(e.target.value)
                }}
                rows={5}
                class="shadow-sm w-full p-2 rounded-sm"
                placeholder="Write some notes here..."
              />
              <div class="italic text-xs">
                Notes are stored in the URL. Try copying the URL into a new tab!
              </div>
            </div>
          </>
        ) : null}
      </div>
      <div>
        <button
          class="bg-blue-500 rounded-sm p-2 uppercase text-white font-black disabled:opacity-50"
          disabled={updateInvoiceMutation.status === 'pending'}
        >
          Save
        </button>
      </div>
      {updateInvoiceMutation.variables?.id === invoice().id ? (
        <div>
          {updateInvoiceMutation.status === 'success' ? (
            <div class="inline-block px-2 py-1 rounded-sm bg-green-500 text-white animate-bounce [animation-iteration-count:2.5] [animation-duration:.3s]">
              Saved!
            </div>
          ) : updateInvoiceMutation.status === 'error' ? (
            <div class="inline-block px-2 py-1 rounded-sm bg-red-500 text-white animate-bounce [animation-iteration-count:2.5] [animation-duration:.3s]">
              Failed to save.
            </div>
          ) : null}
        </div>
      ) : null}
    </form>
  )
}

const usersLayoutRoute = createRoute({
  getParentRoute: () => dashboardLayoutRoute,
  path: 'users',
  validateSearch: z.object({
    usersView: z
      .object({
        sortBy: z.enum(['name', 'id', 'email']).optional(),
        filterBy: z.string().optional(),
      })
      .optional(),
  }).parse,
  search: {
    // Retain the usersView search param while navigating
    // within or to this route (or it's children!)
    middlewares: [retainSearchParams(['usersView'])],
  },
  loaderDeps: ({ search: { usersView } }) => ({
    filterBy: usersView?.filterBy,
    sortBy: usersView?.sortBy ?? 'name',
  }),
  loader: ({ deps }) => fetchUsers(deps),
  component: UsersLayoutComponent,
})

function UsersLayoutComponent() {
  const navigate = useNavigate({ from: usersLayoutRoute.fullPath })
  const search = usersLayoutRoute.useSearch()
  const users = usersLayoutRoute.useLoaderData()
  const sortBy = search().usersView?.sortBy ?? 'name'
  const filterBy = search().usersView?.filterBy

  const [filterDraft, setFilterDraft] = createSignal(filterBy ?? '')

  createEffect(
    on(
      () => filterBy,
      () => {
        setFilterDraft(filterBy ?? '')
      },
    ),
  )

  const sortedUsers = createMemo(() => {
    if (!users) return []

    return !sortBy
      ? users()
      : [...users()].sort((a, b) => {
          return a[sortBy] > b[sortBy] ? 1 : -1
        })
  }, [users, sortBy])

  const filteredUsers = createMemo(() => {
    if (!filterBy) return sortedUsers()

    return sortedUsers().filter((user) =>
      user.name.toLowerCase().includes(filterBy.toLowerCase()),
    )
  }, [sortedUsers, filterBy])

  const setSortBy = (sortBy: UsersViewSortBy) =>
    navigate({
      search: (old) => {
        return {
          ...old,
          usersView: {
            ...(old.usersView ?? {}),
            sortBy,
          },
        }
      },
      replace: true,
    })

  createEffect(
    on(filterDraft, () => {
      navigate({
        search: (old) => {
          return {
            ...old,
            usersView: {
              ...old.usersView,
              filterBy: filterDraft() || undefined,
            },
          }
        },
        replace: true,
      })
    }),
  )

  return (
    <div class="flex-1 flex">
      <div class="divide-y">
        <div class="py-2 px-3 flex gap-2 items-center bg-gray-100 dark:bg-gray-800">
          <div>Sort By:</div>
          <select
            value={sortBy}
            onChange={(e) => setSortBy(e.target.value as UsersViewSortBy)}
            class="flex-1 border p-1 px-2 rounded-sm"
          >
            {['name', 'id', 'email'].map((d) => {
              return <option value={d} children={d} />
            })}
          </select>
        </div>
        <div class="py-2 px-3 flex gap-2 items-center bg-gray-100 dark:bg-gray-800">
          <div>Filter By:</div>
          <input
            value={filterDraft()}
            onChange={(e) => setFilterDraft(e.target.value)}
            placeholder="Search Names..."
            class="min-w-0 flex-1 border p-1 px-2 rounded-sm"
          />
        </div>
        {filteredUsers().map((user) => {
          return (
            <div>
              <Link
                to="/dashboard/users/user"
                search={{
                  userId: user.id,
                }}
                class="block py-2 px-3 text-blue-700"
                activeProps={{ class: `font-bold` }}
              >
                <pre class="text-sm">
                  {user.name}{' '}
                  <MatchRoute
                    to={userRoute.to}
                    search={{
                      userId: user.id,
                    }}
                    pending
                  >
                    {(match) => <Spinner show={!!match} wait="delay-50" />}
                  </MatchRoute>
                </pre>
              </Link>
            </div>
          )
        })}
      </div>
      <div class="flex-initial border-l">
        <Outlet />
      </div>
    </div>
  )
}

const usersIndexRoute = createRoute({
  getParentRoute: () => usersLayoutRoute,
  path: '/',
  component: UsersIndexComponent,
})

function UsersIndexComponent() {
  return (
    <div class="p-2 space-y-2">
      <p>
        Normally, setting default search parameters would either need to be done
        manually in every link to a page, or as a side-effect (not a great
        experience).
      </p>
      <p>
        Instead, we can use <strong>search filters</strong> to provide defaults
        or even persist search params for links to routes (and child routes).
      </p>
      <p>
        A good example of this is the sorting and filtering of the users list.
        In a traditional router, both would be lost while navigating around
        individual users or even changing each sort/filter option unless each
        state was manually passed from the current route into each new link we
        created (that's a lot of tedious and error-prone work). With TanStack
        router and search filters, they are persisted with little effort.
      </p>
    </div>
  )
}

const userRoute = createRoute({
  getParentRoute: () => usersLayoutRoute,
  path: 'user',
  validateSearch: z.object({
    userId: z.number(),
  }),
  loaderDeps: ({ search: { userId } }) => ({
    userId,
  }),
  loader: ({ deps: { userId } }) => fetchUserById(userId),
  component: UserComponent,
})

function UserComponent() {
  const user = userRoute.useLoaderData()

  return (
    <>
      <h4 class="p-2 font-bold">{user()?.name}</h4>
      <pre class="text-sm whitespace-pre-wrap">
        {JSON.stringify(user(), null, 2)}
      </pre>
    </>
  )
}

const expensiveRoute = createRoute({
  getParentRoute: () => rootRoute,
  // Your elements can be asynchronous, which means you can code-split!
  path: 'expensive',
  component: lazyRouteComponent(() => import('./Expensive')),
})

const authPathlessLayoutRoute = createRoute({
  getParentRoute: () => rootRoute,
  id: 'auth',
  // Before loading, authenticate the user via our auth context
  // This will also happen during prefetching (e.g. hovering over links, etc)
  beforeLoad: ({ context, location }) => {
    // If the user is logged out, redirect them to the login page
    if (context.auth.status === 'loggedOut') {
      console.log(location)
      throw redirect({
        to: loginRoute.to,
        search: {
          // Use the current location to power a redirect after login
          // (Do not use `router.state.resolvedLocation` as it can
          // potentially lag behind the actual current location)
          redirect: location.href,
        },
      })
    }

    // Otherwise, return the user in context
    return {
      username: auth.username,
    }
  },
})

const profileRoute = createRoute({
  getParentRoute: () => authPathlessLayoutRoute,
  path: 'profile',
  component: ProfileComponent,
})

function ProfileComponent() {
  const routeContext = profileRoute.useRouteContext()

  return (
    <div class="p-2 space-y-2">
      <div>
        Username:<strong>{routeContext().username}</strong>
      </div>
      <button
        class="text-sm bg-blue-500 text-white border inline-block py-1 px-2 rounded-sm"
        onClick={() => {
          auth.logout()
          router.invalidate()
        }}
      >
        Log out
      </button>
    </div>
  )
}

const loginRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'login',
  validateSearch: z.object({
    redirect: z.string().optional(),
  }),
}).update({
  component: LoginComponent,
})

function LoginComponent() {
  const router = useRouter()
  const routeContext = loginRoute.useRouteContext({
    select: ({ auth }) => ({ auth, status: auth.status }),
  })
  const search = useSearch({ from: loginRoute.fullPath })
  const [username, setUsername] = createSignal('')

  const onSubmit = (e: SubmitEvent) => {
    e.preventDefault()
    routeContext().auth.login(username())
    router.invalidate()
  }

  // Ah, the subtle nuances of client side auth. 🙄
  createRenderEffect(
    on([() => routeContext().status, () => search().redirect], () => {
      const redirect = search().redirect
      if (routeContext().status === 'loggedIn' && redirect) {
        router.history.push(redirect)
      }
    }),
  )

  return routeContext().status === 'loggedIn' ? (
    <div>
      Logged in as <strong>{auth.username}</strong>
      <div class="h-2" />
      <button
        onClick={() => {
          auth.logout()
          router.invalidate()
        }}
        class="text-sm bg-blue-500 text-white border inline-block py-1 px-2 rounded-sm"
      >
        Log out
      </button>
      <div class="h-2" />
    </div>
  ) : (
    <div class="p-2">
      <div>You must log in!</div>
      <div class="h-2" />
      <form onSubmit={onSubmit} class="flex gap-2">
        <input
          value={username()}
          onInput={(e) => setUsername(e.currentTarget.value)}
          placeholder="Username"
          class="border p-1 px-2 rounded-sm"
        />
        <button class="text-sm bg-blue-500 text-white border inline-block py-1 px-2 rounded-sm">
          Login
        </button>
      </form>
    </div>
  )
}

const pathlessLayoutRoute = createRoute({
  getParentRoute: () => rootRoute,
  id: 'pathless-layout',
  component: PathlessLayoutComponent,
})

function PathlessLayoutComponent() {
  return (
    <div>
      <div>Pathless Layout</div>
      <hr />
      <Outlet />
    </div>
  )
}

const pathlessLayoutARoute = createRoute({
  getParentRoute: () => pathlessLayoutRoute,
  path: 'route-a',
  component: PathlessLayoutAComponent,
})

function PathlessLayoutAComponent() {
  return (
    <div>
      <div>I'm A</div>
    </div>
  )
}

const pathlessLayoutBRoute = createRoute({
  getParentRoute: () => pathlessLayoutRoute,
  path: 'route-b',
  component: PathlessLayoutBComponent,
})

function PathlessLayoutBComponent() {
  return (
    <div>
      <div>I'm B</div>
    </div>
  )
}

const routeTree = rootRoute.addChildren([
  indexRoute,
  dashboardLayoutRoute.addChildren([
    dashboardIndexRoute,
    invoicesLayoutRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
    usersLayoutRoute.addChildren([usersIndexRoute, userRoute]),
  ]),
  expensiveRoute,
  authPathlessLayoutRoute.addChildren([profileRoute]),
  loginRoute,
  pathlessLayoutRoute.addChildren([pathlessLayoutARoute, pathlessLayoutBRoute]),
])

const router = createRouter({
  routeTree,
  defaultPendingComponent: () => (
    <div class={`p-2 text-2xl`}>
      <Spinner />
    </div>
  ),
  defaultErrorComponent: ({ error }) => <ErrorComponent error={error} />,
  context: {
    auth: undefined!, // We'll inject this when we render
  },
  defaultPreload: 'intent',
  scrollRestoration: true,
})

declare module '@tanstack/solid-router' {
  interface Register {
    router: typeof router
  }
}

const auth: Auth = {
  status: 'loggedOut',
  username: undefined,
  login: (username: string) => {
    auth.username = username
    auth.status = 'loggedIn'
  },
  logout: () => {
    auth.status = 'loggedOut'
    auth.username = undefined
  },
}

function App() {
  // This stuff is just to tweak our sandbox setup in real-time
  const [loaderDelay, setLoaderDelay] = useSessionStorage('loaderDelay', 500)
  const [pendingMs, setPendingMs] = useSessionStorage('pendingMs', 1000)
  const [pendingMinMs, setPendingMinMs] = useSessionStorage('pendingMinMs', 500)

  return (
    <>
      <div class="text-xs fixed w-52 shadow-md shadow-black/20 rounded-sm bottom-2 left-2 bg-white dark:bg-gray-800 bg-opacity-75 border-b flex flex-col gap-1 flex-wrap items-left divide-y">
        <div class="p-2 space-y-2">
          <div class="flex gap-2">
            <button
              class="bg-blue-500 text-white rounded-sm p-1 px-2"
              onClick={() => {
                setLoaderDelay(150)
              }}
            >
              Fast
            </button>
            <button
              class="bg-blue-500 text-white rounded-sm p-1 px-2"
              onClick={() => {
                setLoaderDelay(500)
              }}
            >
              Fast 3G
            </button>
            <button
              class="bg-blue-500 text-white rounded-sm p-1 px-2"
              onClick={() => {
                setLoaderDelay(2000)
              }}
            >
              Slow 3G
            </button>
          </div>
          <div>
            <div>Loader Delay: {loaderDelay()}ms</div>
            <input
              type="range"
              min="0"
              max="5000"
              step="100"
              value={loaderDelay()}
              onInput={(e) => setLoaderDelay(e.currentTarget.valueAsNumber)}
              class="w-full"
            />
          </div>
        </div>
        <div class="p-2 space-y-2">
          <div class="flex gap-2">
            <button
              class="bg-blue-500 text-white rounded-sm p-1 px-2"
              onClick={() => {
                setPendingMs(1000)
                setPendingMinMs(500)
              }}
            >
              Reset to Default
            </button>
          </div>
          <div>
            <div>defaultPendingMs: {pendingMs()}ms</div>
            <input
              type="range"
              min="0"
              max="5000"
              step="100"
              value={pendingMs()}
              onInput={(e) => setPendingMs(e.currentTarget.valueAsNumber)}
              class="w-full"
            />
          </div>
          <div>
            <div>defaultPendingMinMs: {pendingMinMs()}ms</div>
            <input
              type="range"
              min="0"
              max="5000"
              step="100"
              value={pendingMinMs()}
              onInput={(e) => setPendingMinMs(e.currentTarget.valueAsNumber)}
              class="w-full"
            />
          </div>
        </div>
      </div>
      <RouterProvider
        router={router}
        defaultPreload="intent"
        defaultPendingMs={pendingMs()}
        defaultPendingMinMs={pendingMinMs()}
        context={{
          auth,
        }}
      />
    </>
  )
}

function InvoiceFields({
  invoice,
  disabled,
}: {
  invoice: Invoice
  disabled?: boolean
}) {
  return (
    <div class="space-y-2">
      <h2 class="font-bold text-lg">
        <input
          name="title"
          value={invoice.title || ''}
          placeholder="Invoice Title"
          class="border border-opacity-50 rounded-sm p-2 w-full"
          disabled={disabled}
        />
      </h2>
      <div>
        <textarea
          name="body"
          value={invoice.body || ''}
          rows={6}
          placeholder="Invoice Body..."
          class="border border-opacity-50 p-2 rounded-sm w-full"
          disabled={disabled}
        />
      </div>
    </div>
  )
}

type Auth = {
  login: (username: string) => void
  logout: () => void
  status: 'loggedOut' | 'loggedIn'
  username?: string
}

function Spinner({ show, wait }: { show?: boolean; wait?: `delay-${number}` }) {
  return (
    <div
      class={`inline-block animate-spin px-3 transition ${
        (show ?? true)
          ? `opacity-1 duration-500 ${wait ?? 'delay-300'}`
          : 'duration-500 opacity-0 delay-0'
      }`}
    >
      ⍥
    </div>
  )
}

function useSessionStorage<T>(key: string, initialValue: T) {
  const [state, setState] = createSignal<T>(
    (() => {
      const stored = sessionStorage.getItem(key)
      return stored ? JSON.parse(stored) : initialValue
    })(),
  )

  createEffect(
    on(state, () => {
      sessionStorage.setItem(key, JSON.stringify(state()))
    }),
  )

  return [state, setState] as const
}

const rootElement = document.getElementById('app')!

if (!rootElement.innerHTML) {
  render(() => <App />, rootElement)
}
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import { render } from 'solid-js/web'
import {
  ErrorComponent,
  Link,
  MatchRoute,
  Outlet,
  RouterProvider,
  createRootRouteWithContext,
  createRoute,
  createRouter,
  lazyRouteComponent,
  redirect,
  retainSearchParams,
  useNavigate,
  useRouter,
  useRouterState,
  useSearch,
} from '@tanstack/solid-router'
import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools'
import {
  createEffect,
  createMemo,
  createRenderEffect,
  createSignal,
  on,
} from 'solid-js'
import { z } from 'zod'
import {
  fetchInvoiceById,
  fetchInvoices,
  fetchUserById,
  fetchUsers,
  patchInvoice,
  postInvoice,
} from './mockTodos'
import { useMutation } from './useMutation'
import type { Invoice } from './mockTodos'
import './styles.css'

//

type UsersViewSortBy = 'name' | 'id' | 'email'

const rootRoute = createRootRouteWithContext<{
  auth: Auth
}>()({
  component: RootComponent,
})

function RouterSpinner() {
  const isLoading = useRouterState({ select: (s) => s.status === 'pending' })
  return <Spinner show={isLoading()} />
}

function RootComponent() {
  return (
    <>
      <div class={`min-h-screen flex flex-col`}>
        <div class={`flex items-center border-b gap-2`}>
          <h1 class={`text-3xl p-2`}>Kitchen Sink</h1>
          {/* Show a global spinner when the router is transitioning */}
          <div class={`text-3xl`}>
            <RouterSpinner />
          </div>
        </div>
        <div class={`flex-1 flex`}>
          <div class={`divide-y w-56`}>
            {(
              [
                ['/', 'Home'],
                ['/dashboard', 'Dashboard'],
                ['/expensive', 'Expensive'],
                ['/route-a', 'Pathless Layout A'],
                ['/route-b', 'Pathless Layout B'],
                ['/profile', 'Profile'],
                ...(auth.status === 'loggedOut' ? [['/login', 'Login']] : []),
              ] as const
            ).map(([to, label]) => {
              return (
                <div>
                  <Link
                    to={to}
                    activeOptions={
                      {
                        // If the route points to the root of it's parent,
                        // make sure it's only active if it's exact
                        // exact: to === '.',
                      }
                    }
                    preload="intent"
                    class={`block py-2 px-3 text-blue-700`}
                    // Make "active" links bold
                    activeProps={{ class: `font-bold` }}
                  >
                    {label}
                  </Link>
                </div>
              )
            })}
          </div>
          <div class={`flex-1 border-l`}>
            {/* Render our first route match */}
            <Outlet />
          </div>
        </div>
      </div>
      <TanStackRouterDevtools position="bottom-right" />
    </>
  )
}

const indexRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/',
  component: IndexComponent,
})

function IndexComponent() {
  return (
    <div class={`p-2`}>
      <div class={`text-lg`}>Welcome Home!</div>
      <hr class={`my-2`} />
      <Link
        to={invoiceRoute.to}
        params={{
          invoiceId: 3,
        }}
        class={`py-1 px-2 text-xs bg-blue-500 text-white rounded-full`}
      >
        1 New Invoice
      </Link>
      <hr class={`my-2`} />
      <div class={`max-w-xl`}>
        As you navigate around take note of the UX. It should feel
        suspense-like, where routes are only rendered once all of their data and
        elements are ready.
        <hr class={`my-2`} />
        To exaggerate async effects, play with the artificial request delay
        slider in the bottom-left corner.
        <hr class={`my-2`} />
        The last 2 sliders determine if link-hover preloading is enabled (and
        how long those preloads stick around) and also whether to cache rendered
        route data (and for how long). Both of these default to 0 (or off).
      </div>
    </div>
  )
}

const dashboardLayoutRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'dashboard',
  component: DashboardLayoutComponent,
})

function DashboardLayoutComponent() {
  return (
    <>
      <div class="flex items-center border-b">
        <h2 class="text-xl p-2">Dashboard</h2>
      </div>
      <div class="flex flex-wrap divide-x">
        {(
          [
            ['/dashboard', 'Summary', true],
            ['/dashboard/invoices', 'Invoices'],
            ['/dashboard/users', 'Users'],
          ] as const
        ).map(([to, label, exact]) => {
          return (
            <Link
              to={to}
              activeOptions={{ exact }}
              activeProps={{ class: `font-bold` }}
              class="p-2"
            >
              {label}
            </Link>
          )
        })}
      </div>
      <hr />
      <Outlet />
    </>
  )
}

const dashboardIndexRoute = createRoute({
  getParentRoute: () => dashboardLayoutRoute,
  path: '/',
  loader: () => fetchInvoices(),
  component: DashboardIndexComponent,
})

function DashboardIndexComponent() {
  const invoices = dashboardIndexRoute.useLoaderData()

  return (
    <div class="p-2">
      <div class="p-2">
        Welcome to the dashboard! You have{' '}
        <strong>{invoices.length} total invoices</strong>.
      </div>
    </div>
  )
}

const invoicesLayoutRoute = createRoute({
  getParentRoute: () => dashboardLayoutRoute,
  path: 'invoices',
  loader: () => fetchInvoices(),
  component: InvoicesLayoutComponent,
})

function InvoicesLayoutComponent() {
  const invoices = invoicesLayoutRoute.useLoaderData()

  return (
    <div class="flex-1 flex">
      <div class="divide-y w-48">
        {invoices().map((invoice) => {
          return (
            <div>
              <Link
                to="/dashboard/invoices/$invoiceId"
                params={{
                  invoiceId: invoice.id,
                }}
                preload="intent"
                class="block py-2 px-3 text-blue-700"
                activeProps={{ class: `font-bold ` }}
              >
                <pre class="text-sm">
                  #{invoice.id} - {invoice.title.slice(0, 10)}{' '}
                  {/* {updateSubmission ? (
                      <Spinner />
                    ) : ( */}
                  <MatchRoute
                    to={invoiceRoute.to}
                    params={{
                      invoiceId: invoice.id,
                    }}
                    pending
                  >
                    {(match) => <Spinner show={!!match} wait="delay-50" />}
                  </MatchRoute>
                  {/* )} */}
                </pre>
              </Link>
            </div>
          )
        })}
      </div>
      <div class="flex-1 border-l">
        <Outlet />
      </div>
    </div>
  )
}

const invoicesIndexRoute = createRoute({
  getParentRoute: () => invoicesLayoutRoute,
  path: '/',
  component: InvoicesIndexComponent,
})

function InvoicesIndexComponent() {
  const createInvoiceMutation = useMutation({
    fn: postInvoice,
    onSuccess: () => router.invalidate(),
  })

  return (
    <>
      <div class="p-2">
        <form
          onSubmit={(event) => {
            event.preventDefault()
            event.stopPropagation()
            const formData = new FormData(event.target as HTMLFormElement)
            createInvoiceMutation.mutate({
              title: formData.get('title') as string,
              body: formData.get('body') as string,
            })
          }}
          class="space-y-2"
        >
          <div>Create a new Invoice:</div>
          <InvoiceFields invoice={{} as Invoice} />
          <div>
            <button
              class="bg-blue-500 rounded-sm p-2 uppercase text-white font-black disabled:opacity-50"
              disabled={createInvoiceMutation.status === 'pending'}
            >
              {createInvoiceMutation.status === 'pending' ? (
                <>
                  Creating <Spinner />
                </>
              ) : (
                'Create'
              )}
            </button>
          </div>
          {createInvoiceMutation.status === 'success' ? (
            <div class="inline-block px-2 py-1 rounded-sm bg-green-500 text-white animate-bounce [animation-iteration-count:2.5] [animation-duration:.3s]">
              Created!
            </div>
          ) : createInvoiceMutation.status === 'error' ? (
            <div class="inline-block px-2 py-1 rounded-sm bg-red-500 text-white animate-bounce [animation-iteration-count:2.5] [animation-duration:.3s]">
              Failed to create.
            </div>
          ) : null}
        </form>
      </div>
    </>
  )
}

const invoiceRoute = createRoute({
  getParentRoute: () => invoicesLayoutRoute,
  path: '$invoiceId',
  params: {
    parse: (params) => ({
      invoiceId: z.number().int().parse(Number(params.invoiceId)),
    }),
    stringify: ({ invoiceId }) => ({ invoiceId: `${invoiceId}` }),
  },
  validateSearch: (search) =>
    z
      .object({
        showNotes: z.boolean().optional(),
        notes: z.string().optional(),
      })
      .parse(search),
  loader: ({ params: { invoiceId } }) => fetchInvoiceById(invoiceId),
  component: InvoiceComponent,
  pendingComponent: () => <Spinner />,
})

function InvoiceComponent() {
  const search = invoiceRoute.useSearch()
  const navigate = useNavigate({ from: invoiceRoute.fullPath })
  const invoice = invoiceRoute.useLoaderData()
  const updateInvoiceMutation = useMutation({
    fn: patchInvoice,
    onSuccess: () => router.invalidate(),
  })
  const [notes, setNotes] = createSignal(search().notes ?? '')
  createEffect(
    on(notes, () => {
      navigate({
        search: (old) => ({
          ...old,
          notes: notes() ? notes() : undefined,
        }),
        params: true,
        replace: true,
      })
    }),
  )

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault()
        event.stopPropagation()
        const formData = new FormData(event.target as HTMLFormElement)
        updateInvoiceMutation.mutate({
          id: invoice().id,
          title: formData.get('title') as string,
          body: formData.get('body') as string,
        })
      }}
      class="p-2 space-y-2"
    >
      <InvoiceFields
        invoice={invoice()}
        disabled={updateInvoiceMutation.status === 'pending'}
      />
      <div>
        <Link
          search={(old) => ({
            ...old,
            showNotes: old.showNotes ? undefined : true,
          })}
          class="text-blue-700"
          from={invoiceRoute.fullPath}
          params={true}
        >
          {search().showNotes ? 'Close Notes' : 'Show Notes'}{' '}
        </Link>
        {search().showNotes ? (
          <>
            <div>
              <div class="h-2" />
              <textarea
                value={notes()}
                onChange={(e) => {
                  setNotes(e.target.value)
                }}
                rows={5}
                class="shadow-sm w-full p-2 rounded-sm"
                placeholder="Write some notes here..."
              />
              <div class="italic text-xs">
                Notes are stored in the URL. Try copying the URL into a new tab!
              </div>
            </div>
          </>
        ) : null}
      </div>
      <div>
        <button
          class="bg-blue-500 rounded-sm p-2 uppercase text-white font-black disabled:opacity-50"
          disabled={updateInvoiceMutation.status === 'pending'}
        >
          Save
        </button>
      </div>
      {updateInvoiceMutation.variables?.id === invoice().id ? (
        <div>
          {updateInvoiceMutation.status === 'success' ? (
            <div class="inline-block px-2 py-1 rounded-sm bg-green-500 text-white animate-bounce [animation-iteration-count:2.5] [animation-duration:.3s]">
              Saved!
            </div>
          ) : updateInvoiceMutation.status === 'error' ? (
            <div class="inline-block px-2 py-1 rounded-sm bg-red-500 text-white animate-bounce [animation-iteration-count:2.5] [animation-duration:.3s]">
              Failed to save.
            </div>
          ) : null}
        </div>
      ) : null}
    </form>
  )
}

const usersLayoutRoute = createRoute({
  getParentRoute: () => dashboardLayoutRoute,
  path: 'users',
  validateSearch: z.object({
    usersView: z
      .object({
        sortBy: z.enum(['name', 'id', 'email']).optional(),
        filterBy: z.string().optional(),
      })
      .optional(),
  }).parse,
  search: {
    // Retain the usersView search param while navigating
    // within or to this route (or it's children!)
    middlewares: [retainSearchParams(['usersView'])],
  },
  loaderDeps: ({ search: { usersView } }) => ({
    filterBy: usersView?.filterBy,
    sortBy: usersView?.sortBy ?? 'name',
  }),
  loader: ({ deps }) => fetchUsers(deps),
  component: UsersLayoutComponent,
})

function UsersLayoutComponent() {
  const navigate = useNavigate({ from: usersLayoutRoute.fullPath })
  const search = usersLayoutRoute.useSearch()
  const users = usersLayoutRoute.useLoaderData()
  const sortBy = search().usersView?.sortBy ?? 'name'
  const filterBy = search().usersView?.filterBy

  const [filterDraft, setFilterDraft] = createSignal(filterBy ?? '')

  createEffect(
    on(
      () => filterBy,
      () => {
        setFilterDraft(filterBy ?? '')
      },
    ),
  )

  const sortedUsers = createMemo(() => {
    if (!users) return []

    return !sortBy
      ? users()
      : [...users()].sort((a, b) => {
          return a[sortBy] > b[sortBy] ? 1 : -1
        })
  }, [users, sortBy])

  const filteredUsers = createMemo(() => {
    if (!filterBy) return sortedUsers()

    return sortedUsers().filter((user) =>
      user.name.toLowerCase().includes(filterBy.toLowerCase()),
    )
  }, [sortedUsers, filterBy])

  const setSortBy = (sortBy: UsersViewSortBy) =>
    navigate({
      search: (old) => {
        return {
          ...old,
          usersView: {
            ...(old.usersView ?? {}),
            sortBy,
          },
        }
      },
      replace: true,
    })

  createEffect(
    on(filterDraft, () => {
      navigate({
        search: (old) => {
          return {
            ...old,
            usersView: {
              ...old.usersView,
              filterBy: filterDraft() || undefined,
            },
          }
        },
        replace: true,
      })
    }),
  )

  return (
    <div class="flex-1 flex">
      <div class="divide-y">
        <div class="py-2 px-3 flex gap-2 items-center bg-gray-100 dark:bg-gray-800">
          <div>Sort By:</div>
          <select
            value={sortBy}
            onChange={(e) => setSortBy(e.target.value as UsersViewSortBy)}
            class="flex-1 border p-1 px-2 rounded-sm"
          >
            {['name', 'id', 'email'].map((d) => {
              return <option value={d} children={d} />
            })}
          </select>
        </div>
        <div class="py-2 px-3 flex gap-2 items-center bg-gray-100 dark:bg-gray-800">
          <div>Filter By:</div>
          <input
            value={filterDraft()}
            onChange={(e) => setFilterDraft(e.target.value)}
            placeholder="Search Names..."
            class="min-w-0 flex-1 border p-1 px-2 rounded-sm"
          />
        </div>
        {filteredUsers().map((user) => {
          return (
            <div>
              <Link
                to="/dashboard/users/user"
                search={{
                  userId: user.id,
                }}
                class="block py-2 px-3 text-blue-700"
                activeProps={{ class: `font-bold` }}
              >
                <pre class="text-sm">
                  {user.name}{' '}
                  <MatchRoute
                    to={userRoute.to}
                    search={{
                      userId: user.id,
                    }}
                    pending
                  >
                    {(match) => <Spinner show={!!match} wait="delay-50" />}
                  </MatchRoute>
                </pre>
              </Link>
            </div>
          )
        })}
      </div>
      <div class="flex-initial border-l">
        <Outlet />
      </div>
    </div>
  )
}

const usersIndexRoute = createRoute({
  getParentRoute: () => usersLayoutRoute,
  path: '/',
  component: UsersIndexComponent,
})

function UsersIndexComponent() {
  return (
    <div class="p-2 space-y-2">
      <p>
        Normally, setting default search parameters would either need to be done
        manually in every link to a page, or as a side-effect (not a great
        experience).
      </p>
      <p>
        Instead, we can use <strong>search filters</strong> to provide defaults
        or even persist search params for links to routes (and child routes).
      </p>
      <p>
        A good example of this is the sorting and filtering of the users list.
        In a traditional router, both would be lost while navigating around
        individual users or even changing each sort/filter option unless each
        state was manually passed from the current route into each new link we
        created (that's a lot of tedious and error-prone work). With TanStack
        router and search filters, they are persisted with little effort.
      </p>
    </div>
  )
}

const userRoute = createRoute({
  getParentRoute: () => usersLayoutRoute,
  path: 'user',
  validateSearch: z.object({
    userId: z.number(),
  }),
  loaderDeps: ({ search: { userId } }) => ({
    userId,
  }),
  loader: ({ deps: { userId } }) => fetchUserById(userId),
  component: UserComponent,
})

function UserComponent() {
  const user = userRoute.useLoaderData()

  return (
    <>
      <h4 class="p-2 font-bold">{user()?.name}</h4>
      <pre class="text-sm whitespace-pre-wrap">
        {JSON.stringify(user(), null, 2)}
      </pre>
    </>
  )
}

const expensiveRoute = createRoute({
  getParentRoute: () => rootRoute,
  // Your elements can be asynchronous, which means you can code-split!
  path: 'expensive',
  component: lazyRouteComponent(() => import('./Expensive')),
})

const authPathlessLayoutRoute = createRoute({
  getParentRoute: () => rootRoute,
  id: 'auth',
  // Before loading, authenticate the user via our auth context
  // This will also happen during prefetching (e.g. hovering over links, etc)
  beforeLoad: ({ context, location }) => {
    // If the user is logged out, redirect them to the login page
    if (context.auth.status === 'loggedOut') {
      console.log(location)
      throw redirect({
        to: loginRoute.to,
        search: {
          // Use the current location to power a redirect after login
          // (Do not use `router.state.resolvedLocation` as it can
          // potentially lag behind the actual current location)
          redirect: location.href,
        },
      })
    }

    // Otherwise, return the user in context
    return {
      username: auth.username,
    }
  },
})

const profileRoute = createRoute({
  getParentRoute: () => authPathlessLayoutRoute,
  path: 'profile',
  component: ProfileComponent,
})

function ProfileComponent() {
  const routeContext = profileRoute.useRouteContext()

  return (
    <div class="p-2 space-y-2">
      <div>
        Username:<strong>{routeContext().username}</strong>
      </div>
      <button
        class="text-sm bg-blue-500 text-white border inline-block py-1 px-2 rounded-sm"
        onClick={() => {
          auth.logout()
          router.invalidate()
        }}
      >
        Log out
      </button>
    </div>
  )
}

const loginRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'login',
  validateSearch: z.object({
    redirect: z.string().optional(),
  }),
}).update({
  component: LoginComponent,
})

function LoginComponent() {
  const router = useRouter()
  const routeContext = loginRoute.useRouteContext({
    select: ({ auth }) => ({ auth, status: auth.status }),
  })
  const search = useSearch({ from: loginRoute.fullPath })
  const [username, setUsername] = createSignal('')

  const onSubmit = (e: SubmitEvent) => {
    e.preventDefault()
    routeContext().auth.login(username())
    router.invalidate()
  }

  // Ah, the subtle nuances of client side auth. 🙄
  createRenderEffect(
    on([() => routeContext().status, () => search().redirect], () => {
      const redirect = search().redirect
      if (routeContext().status === 'loggedIn' && redirect) {
        router.history.push(redirect)
      }
    }),
  )

  return routeContext().status === 'loggedIn' ? (
    <div>
      Logged in as <strong>{auth.username}</strong>
      <div class="h-2" />
      <button
        onClick={() => {
          auth.logout()
          router.invalidate()
        }}
        class="text-sm bg-blue-500 text-white border inline-block py-1 px-2 rounded-sm"
      >
        Log out
      </button>
      <div class="h-2" />
    </div>
  ) : (
    <div class="p-2">
      <div>You must log in!</div>
      <div class="h-2" />
      <form onSubmit={onSubmit} class="flex gap-2">
        <input
          value={username()}
          onInput={(e) => setUsername(e.currentTarget.value)}
          placeholder="Username"
          class="border p-1 px-2 rounded-sm"
        />
        <button class="text-sm bg-blue-500 text-white border inline-block py-1 px-2 rounded-sm">
          Login
        </button>
      </form>
    </div>
  )
}

const pathlessLayoutRoute = createRoute({
  getParentRoute: () => rootRoute,
  id: 'pathless-layout',
  component: PathlessLayoutComponent,
})

function PathlessLayoutComponent() {
  return (
    <div>
      <div>Pathless Layout</div>
      <hr />
      <Outlet />
    </div>
  )
}

const pathlessLayoutARoute = createRoute({
  getParentRoute: () => pathlessLayoutRoute,
  path: 'route-a',
  component: PathlessLayoutAComponent,
})

function PathlessLayoutAComponent() {
  return (
    <div>
      <div>I'm A</div>
    </div>
  )
}

const pathlessLayoutBRoute = createRoute({
  getParentRoute: () => pathlessLayoutRoute,
  path: 'route-b',
  component: PathlessLayoutBComponent,
})

function PathlessLayoutBComponent() {
  return (
    <div>
      <div>I'm B</div>
    </div>
  )
}

const routeTree = rootRoute.addChildren([
  indexRoute,
  dashboardLayoutRoute.addChildren([
    dashboardIndexRoute,
    invoicesLayoutRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
    usersLayoutRoute.addChildren([usersIndexRoute, userRoute]),
  ]),
  expensiveRoute,
  authPathlessLayoutRoute.addChildren([profileRoute]),
  loginRoute,
  pathlessLayoutRoute.addChildren([pathlessLayoutARoute, pathlessLayoutBRoute]),
])

const router = createRouter({
  routeTree,
  defaultPendingComponent: () => (
    <div class={`p-2 text-2xl`}>
      <Spinner />
    </div>
  ),
  defaultErrorComponent: ({ error }) => <ErrorComponent error={error} />,
  context: {
    auth: undefined!, // We'll inject this when we render
  },
  defaultPreload: 'intent',
  scrollRestoration: true,
})

declare module '@tanstack/solid-router' {
  interface Register {
    router: typeof router
  }
}

const auth: Auth = {
  status: 'loggedOut',
  username: undefined,
  login: (username: string) => {
    auth.username = username
    auth.status = 'loggedIn'
  },
  logout: () => {
    auth.status = 'loggedOut'
    auth.username = undefined
  },
}

function App() {
  // This stuff is just to tweak our sandbox setup in real-time
  const [loaderDelay, setLoaderDelay] = useSessionStorage('loaderDelay', 500)
  const [pendingMs, setPendingMs] = useSessionStorage('pendingMs', 1000)
  const [pendingMinMs, setPendingMinMs] = useSessionStorage('pendingMinMs', 500)

  return (
    <>
      <div class="text-xs fixed w-52 shadow-md shadow-black/20 rounded-sm bottom-2 left-2 bg-white dark:bg-gray-800 bg-opacity-75 border-b flex flex-col gap-1 flex-wrap items-left divide-y">
        <div class="p-2 space-y-2">
          <div class="flex gap-2">
            <button
              class="bg-blue-500 text-white rounded-sm p-1 px-2"
              onClick={() => {
                setLoaderDelay(150)
              }}
            >
              Fast
            </button>
            <button
              class="bg-blue-500 text-white rounded-sm p-1 px-2"
              onClick={() => {
                setLoaderDelay(500)
              }}
            >
              Fast 3G
            </button>
            <button
              class="bg-blue-500 text-white rounded-sm p-1 px-2"
              onClick={() => {
                setLoaderDelay(2000)
              }}
            >
              Slow 3G
            </button>
          </div>
          <div>
            <div>Loader Delay: {loaderDelay()}ms</div>
            <input
              type="range"
              min="0"
              max="5000"
              step="100"
              value={loaderDelay()}
              onInput={(e) => setLoaderDelay(e.currentTarget.valueAsNumber)}
              class="w-full"
            />
          </div>
        </div>
        <div class="p-2 space-y-2">
          <div class="flex gap-2">
            <button
              class="bg-blue-500 text-white rounded-sm p-1 px-2"
              onClick={() => {
                setPendingMs(1000)
                setPendingMinMs(500)
              }}
            >
              Reset to Default
            </button>
          </div>
          <div>
            <div>defaultPendingMs: {pendingMs()}ms</div>
            <input
              type="range"
              min="0"
              max="5000"
              step="100"
              value={pendingMs()}
              onInput={(e) => setPendingMs(e.currentTarget.valueAsNumber)}
              class="w-full"
            />
          </div>
          <div>
            <div>defaultPendingMinMs: {pendingMinMs()}ms</div>
            <input
              type="range"
              min="0"
              max="5000"
              step="100"
              value={pendingMinMs()}
              onInput={(e) => setPendingMinMs(e.currentTarget.valueAsNumber)}
              class="w-full"
            />
          </div>
        </div>
      </div>
      <RouterProvider
        router={router}
        defaultPreload="intent"
        defaultPendingMs={pendingMs()}
        defaultPendingMinMs={pendingMinMs()}
        context={{
          auth,
        }}
      />
    </>
  )
}

function InvoiceFields({
  invoice,
  disabled,
}: {
  invoice: Invoice
  disabled?: boolean
}) {
  return (
    <div class="space-y-2">
      <h2 class="font-bold text-lg">
        <input
          name="title"
          value={invoice.title || ''}
          placeholder="Invoice Title"
          class="border border-opacity-50 rounded-sm p-2 w-full"
          disabled={disabled}
        />
      </h2>
      <div>
        <textarea
          name="body"
          value={invoice.body || ''}
          rows={6}
          placeholder="Invoice Body..."
          class="border border-opacity-50 p-2 rounded-sm w-full"
          disabled={disabled}
        />
      </div>
    </div>
  )
}

type Auth = {
  login: (username: string) => void
  logout: () => void
  status: 'loggedOut' | 'loggedIn'
  username?: string
}

function Spinner({ show, wait }: { show?: boolean; wait?: `delay-${number}` }) {
  return (
    <div
      class={`inline-block animate-spin px-3 transition ${
        (show ?? true)
          ? `opacity-1 duration-500 ${wait ?? 'delay-300'}`
          : 'duration-500 opacity-0 delay-0'
      }`}
    >
      ⍥
    </div>
  )
}

function useSessionStorage<T>(key: string, initialValue: T) {
  const [state, setState] = createSignal<T>(
    (() => {
      const stored = sessionStorage.getItem(key)
      return stored ? JSON.parse(stored) : initialValue
    })(),
  )

  createEffect(
    on(state, () => {
      sessionStorage.setItem(key, JSON.stringify(state()))
    }),
  )

  return [state, setState] as const
}

const rootElement = document.getElementById('app')!

if (!rootElement.innerHTML) {
  render(() => <App />, rootElement)
}
Subscribe to Bytes

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

Bytes

No spam. Unsubscribe at any time.

Subscribe to Bytes

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

Bytes

No spam. Unsubscribe at any time.