···11-Welcome to your new TanStack app!
22-33-# Getting Started
44-55-To run this application:
66-77-```bash
88-npm install
99-npm run dev
1010-```
1111-1212-# Building For Production
1313-1414-To build this application for production:
1515-1616-```bash
1717-npm run build
1818-```
1919-2020-## Testing
2121-2222-This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with:
2323-2424-```bash
2525-npm run test
2626-```
11+# deck belcher
2722828-## Styling
33+**[deckbelcher.com](https://deckbelcher.com)**
2943030-This project uses [Tailwind CSS](https://tailwindcss.com/) for styling.
55+deckbelcher is a social decklist builder, built on top of atproto.
31677+If you've ever used a tool like moxfield, archidekt, tappedout, deckstats... this aims to replace it.
3283333-## Linting & Formatting
3434-3535-This project uses [Biome](https://biomejs.dev/) for linting and formatting. The following scripts are available:
99+You can see the lexicons [here](./lexicons/) which are derived from typespec [here](./typelex/).
36101111+Perhaps the most interesting non-atproto thing here is the local card search engine. Card data is loaded into a SharedWorker in the background ([here](./src/workers/cards.worker.ts)) and queried and cached in the site with tanstack query. During SSR, binary search of a map of sorted UUIDs -> chunk id + text range allows loading and parsing a minimal amount of JSON (parsing all JSON is extremely slow and won't fit in the CF workers memory limit) to preload these queries. This creates a rather seamless experience, and I find you can't tell that the magic trick is happening unless you look for it. Chunks are content hashed and sorted, so updates usually only require refetching a couple chunks. Volatile data like pricing is split into its own chunk, otherwise chunk caching is essentially moot.
37123838-```bash
3939-npm run lint
4040-npm run format
4141-npm run check
4213```
4343-4444-4545-4646-## Routing
4747-This project uses [TanStack Router](https://tanstack.com/router). The initial setup is a file based router. Which means that the routes are managed as files in `src/routes`.
4848-4949-### Adding A Route
5050-5151-To add a new route to your application just add another a new file in the `./src/routes` directory.
5252-5353-TanStack will automatically generate the content of the route file for you.
5454-5555-Now that you have two routes you can use a `Link` component to navigate between them.
5656-5757-### Adding Links
5858-5959-To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`.
6060-6161-```tsx
6262-import { Link } from "@tanstack/react-router";
1414+getCardById("abc-123")
1515+ โ
1616+ โผ
1717+โโโโโโโโโโโโโโโโโโโโโ
1818+โ card LRU cache โโโhitโโโถ return Card
1919+โ (10k cards) โ
2020+โโโโโโโโโโโโโโโโโโโโโ
2121+ โ miss
2222+ โผ
2323+โโโโโโโโโโโโโโโโโโโโโ
2424+โ cards-byteindex โ binary search sorted UUIDs
2525+โ .bin โ 25 bytes/record: UUID(16) + chunk(1) + offset(4) + len(4)
2626+โโโโโโโโโโโโโโโโโโโโโ
2727+ โ
2828+ โผ
2929+ { chunk: 42, offset: 81920, len: 2048 }
3030+ โ
3131+ โผ
3232+โโโโโโโโโโโโโโโโโโโโโ
3333+โ chunk LRU cache โโโhitโโโถ use cached chunk text
3434+โ (12 chunks) โ
3535+โโโโโโโโโโโโโโโโโโโโโ
3636+ โ miss
3737+ โผ
3838+ fetch cards/cards-042-a1b2c3.json
3939+ โ
4040+ โผ
4141+ chunkText.slice(81920, 81920 + 2048)
4242+ โ
4343+ โผ
4444+ JSON.parse โโโถ cache โโโถ return Card
6345```
64466565-Then anywhere in your JSX you can use it like so:
4747+On the client, a [SharedWorker](https://caniuse.com/sharedworkers) (or regular Worker on Android) loads everything into memory at startup:
66486767-```tsx
6868-<Link to="/about">About</Link>
6949```
7070-7171-This will create a link that will navigate to the `/about` route.
7272-7373-More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent).
7474-7575-### Using A Layout
7676-7777-In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you use the `<Outlet />` component.
7878-7979-Here is an example layout that includes a header:
8080-8181-```tsx
8282-import { Outlet, createRootRoute } from '@tanstack/react-router'
8383-import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
8484-8585-import { Link } from "@tanstack/react-router";
8686-8787-export const Route = createRootRoute({
8888- component: () => (
8989- <>
9090- <header>
9191- <nav>
9292- <Link to="/">Home</Link>
9393- <Link to="/about">About</Link>
9494- </nav>
9595- </header>
9696- <Outlet />
9797- <TanStackRouterDevtools />
9898- </>
9999- ),
100100-})
101101-```
102102-103103-The `<TanStackRouterDevtools />` component is not required so you can remove it if you don't want it in your layout.
104104-105105-More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts).
106106-107107-108108-## Data Fetching
109109-110110-There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered.
111111-112112-For example:
113113-114114-```tsx
115115-const peopleRoute = createRoute({
116116- getParentRoute: () => rootRoute,
117117- path: "/people",
118118- loader: async () => {
119119- const response = await fetch("https://swapi.dev/api/people");
120120- return response.json() as Promise<{
121121- results: {
122122- name: string;
123123- }[];
124124- }>;
125125- },
126126- component: () => {
127127- const data = peopleRoute.useLoaderData();
128128- return (
129129- <ul>
130130- {data.results.map((person) => (
131131- <li key={person.name}>{person.name}</li>
132132- ))}
133133- </ul>
134134- );
135135- },
136136-});
137137-```
138138-139139-Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters).
140140-141141-### React-Query
142142-143143-React-Query is an excellent addition or alternative to route loading and integrating it into you application is a breeze.
144144-145145-First add your dependencies:
146146-147147-```bash
148148-npm install @tanstack/react-query @tanstack/react-query-devtools
149149-```
150150-151151-Next we'll need to create a query client and provider. We recommend putting those in `main.tsx`.
152152-153153-```tsx
154154-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
155155-156156-// ...
157157-158158-const queryClient = new QueryClient();
159159-160160-// ...
161161-162162-if (!rootElement.innerHTML) {
163163- const root = ReactDOM.createRoot(rootElement);
164164-165165- root.render(
166166- <QueryClientProvider client={queryClient}>
167167- <RouterProvider router={router} />
168168- </QueryClientProvider>
169169- );
170170-}
171171-```
172172-173173-You can also add TanStack Query Devtools to the root route (optional).
174174-175175-```tsx
176176-import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
177177-178178-const rootRoute = createRootRoute({
179179- component: () => (
180180- <>
181181- <Outlet />
182182- <ReactQueryDevtools buttonPosition="top-right" />
183183- <TanStackRouterDevtools />
184184- </>
185185- ),
186186-});
5050+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
5151+โ SharedWorker init โ
5252+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
5353+ โ
5454+ โโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโ
5555+ โผ โผ โผ
5656+ fetch chunk 0 fetch chunk 1 ... fetch chunk N (parallel)
5757+ โ โ โ
5858+ โโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโ
5959+ โผ
6060+ merge into cards: Record<id, Card>
6161+ โ
6262+ โโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโ
6363+ โผ โผ โผ
6464+ build id index build oracle build MiniSearch
6565+ Map<id, Card> โ printings fuzzy index
6666+ โ
6767+ โผ
6868+ ~115k cards in memory
6969+ ready for queries
18770```
18871189189-Now you can use `useQuery` to fetch your data.
190190-191191-```tsx
192192-import { useQuery } from "@tanstack/react-query";
193193-194194-import "./App.css";
195195-196196-function App() {
197197- const { data } = useQuery({
198198- queryKey: ["people"],
199199- queryFn: () =>
200200- fetch("https://swapi.dev/api/people")
201201- .then((res) => res.json())
202202- .then((data) => data.results as { name: string }[]),
203203- initialData: [],
204204- });
205205-206206- return (
207207- <div>
208208- <ul>
209209- {data.map((person) => (
210210- <li key={person.name}>{person.name}</li>
211211- ))}
212212- </ul>
213213- </div>
214214- );
215215-}
216216-217217-export default App;
21872```
219219-220220-You can find out everything you need to know on how to use React-Query in the [React-Query documentation](https://tanstack.com/query/latest/docs/framework/react/overview).
221221-222222-## State Management
223223-224224-Another common requirement for React applications is state management. There are many options for state management in React. TanStack Store provides a great starting point for your project.
225225-226226-First you need to add TanStack Store as a dependency:
227227-228228-```bash
229229-npm install @tanstack/store
7373+searchCards("lightning bolt")
7474+ โ
7575+ โผ
7676+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
7777+โ main thread โ
7878+โ TanStack Query โโโถ Comlink RPC call โ
7979+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
8080+ โ postMessage
8181+ โผ
8282+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
8383+โ SharedWorker โ
8484+โ โ
8585+โ MiniSearch.search("lightning bolt") โ
8686+โ โ โ
8787+โ โผ โ
8888+โ filter by restrictions (format, CI) โ
8989+โ โ โ
9090+โ โผ โ
9191+โ return Card[] โ
9292+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
9393+ โ postMessage
9494+ โผ
9595+ results hydrated in UI
23096```
23197232232-Now let's create a simple counter in the `src/App.tsx` file as a demonstration.
9898+Together, the two paths look like this:
23399234234-```tsx
235235-import { useStore } from "@tanstack/react-store";
236236-import { Store } from "@tanstack/store";
237237-import "./App.css";
238238-239239-const countStore = new Store(0);
240240-241241-function App() {
242242- const count = useStore(countStore);
243243- return (
244244- <div>
245245- <button onClick={() => countStore.setState((n) => n + 1)}>
246246- Increment - {count}
247247- </button>
248248- </div>
249249- );
250250-}
251251-252252-export default App;
253100```
254254-255255-One of the many nice features of TanStack Store is the ability to derive state from other state. That derived state will update when the base state updates.
256256-257257-Let's check this out by doubling the count using derived state.
258258-259259-```tsx
260260-import { useStore } from "@tanstack/react-store";
261261-import { Store, Derived } from "@tanstack/store";
262262-import "./App.css";
263263-264264-const countStore = new Store(0);
265265-266266-const doubledStore = new Derived({
267267- fn: () => countStore.state * 2,
268268- deps: [countStore],
269269-});
270270-doubledStore.mount();
271271-272272-function App() {
273273- const count = useStore(countStore);
274274- const doubledCount = useStore(doubledStore);
275275-276276- return (
277277- <div>
278278- <button onClick={() => countStore.setState((n) => n + 1)}>
279279- Increment - {count}
280280- </button>
281281- <div>Doubled - {doubledCount}</div>
282282- </div>
283283- );
284284-}
285285-286286-export default App;
101101+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
102102+ โ public/data/cards/ โ
103103+ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
104104+ โ โ cards-000-xxx.json โ โ
105105+ โ โ cards-001-xxx.json โ โ
106106+ โ โ ... โ โ
107107+ โ โ cards-NNN-xxx.json โ โ
108108+ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ
109109+ โ โ cards-byteindex.bin โ โ
110110+ โ โ indexes.json โ โ
111111+ โ โ volatile.bin โ โ
112112+ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
113113+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
114114+ โ โ
115115+ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
116116+ โ SSR: binary search โ client: load all
117117+ โ + byte slice โ into worker
118118+ โผ โผ
119119+โโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโ
120120+โ CF Worker (SSR) โ โ SharedWorker/Worker โ
121121+โ โ โ โ
122122+โ byteindex lookup O(logn)โ โ ~115k cards in RAM โ
123123+โ parse single card โ โ MiniSearch index โ
124124+โ LRU cache (cards+chunks)โ โ scryfall syntax engine โ
125125+โโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโ
126126+ โ โ
127127+ โโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโ
128128+ โผ
129129+ TanStack Query cache unifies both
130130+ (SSR preloads, client hydrates)
287131```
288132289289-We use the `Derived` class to create a new store that is derived from another store. The `Derived` class has a `mount` method that will start the derived store updating.
133133+Once you have all the data in memory, a lot of things get easy. For example, we are able to do [MiniSearch](https://github.com/lucaong/minisearch) powered fuzzy search over cards in near real time, and implement a scryfall query engine and run it over the cards in memory. We can show a virtualized list of all results, and only copy the details for cards across the IPC barrier when they are on screen. A user on 3G can add cards to their decklist or check the language of a card, without enduring the latency of their connection, as long as they had the chunks cached. High latency 4G connections are much more tolerable. Total data over the wire is ~140mb, which is both a lot (sooo much text) and only a little (most sites, including this one, show cards via images, which quickly add up to exceed this amount).
290134291291-Once we've created the derived store we can use it in the `App` component just like we would any other store using the `useStore` hook.
135135+Even though card data lives locally, we still rely on scryfall for their card CDN. This project is only possible because they are so generous with their data export. You can see the script that processes it [here](./scripts/download-scryfall.ts).
292136293293-You can find out everything you need to know on how to use TanStack Store in the [TanStack Store documentation](https://tanstack.com/store/latest).
137137+## Further Reading
294138295295-# Demo files
139139+More detailed docs live in `.claude/` although they were written (by claude) to help claude keep track of the finer details of these systems:
296140297297-Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed.
141141+- [CARD_DATA.md](./.claude/CARD_DATA.md) - card data pipeline and provider architecture
142142+- [SEARCH.md](./.claude/SEARCH.md) - scryfall-like query engine (lexer โ parser โ matcher)
143143+- [ATPROTO.md](./.claude/ATPROTO.md) - AT Protocol integration, PDS writes, Slingshot reads
144144+- [DECK_VALIDATION.md](./.claude/DECK_VALIDATION.md) - format rules with MTG comprehensive rules citations
145145+- [DECK_FORMATS.md](./.claude/DECK_FORMATS.md) - import/export format comparison (Arena, Moxfield, MTGO, etc.)
298146299299-# Learn More
147147+## Beware!
300148301301-You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com).
149149+While I have reviewed all the code, the *entirety* of the code in this repo was written by claude. I wrote *most of* the prose, like this README (I hate being made to read someone else's LLM output, and I try not to be a hypocrite). I feel that using claude to write this allowed me to take on developer QOL, powerful UX, and extensive testing that I would not have otherwise--but I also feel it's worth being upfront that the workflow here was iterative reviews with claude, feature by feature, rather than by hand.