๐Ÿ‘๏ธ
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

readme

+123 -275
+123 -275
README.md
··· 1 - Welcome to your new TanStack app! 2 - 3 - # Getting Started 4 - 5 - To run this application: 6 - 7 - ```bash 8 - npm install 9 - npm run dev 10 - ``` 11 - 12 - # Building For Production 13 - 14 - To build this application for production: 15 - 16 - ```bash 17 - npm run build 18 - ``` 19 - 20 - ## Testing 21 - 22 - This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with: 23 - 24 - ```bash 25 - npm run test 26 - ``` 1 + # deck belcher 27 2 28 - ## Styling 3 + **[deckbelcher.com](https://deckbelcher.com)** 29 4 30 - This project uses [Tailwind CSS](https://tailwindcss.com/) for styling. 5 + deckbelcher is a social decklist builder, built on top of atproto. 31 6 7 + If you've ever used a tool like moxfield, archidekt, tappedout, deckstats... this aims to replace it. 32 8 33 - ## Linting & Formatting 34 - 35 - This project uses [Biome](https://biomejs.dev/) for linting and formatting. The following scripts are available: 9 + You can see the lexicons [here](./lexicons/) which are derived from typespec [here](./typelex/). 36 10 11 + 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. 37 12 38 - ```bash 39 - npm run lint 40 - npm run format 41 - npm run check 42 13 ``` 43 - 44 - 45 - 46 - ## Routing 47 - 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`. 48 - 49 - ### Adding A Route 50 - 51 - To add a new route to your application just add another a new file in the `./src/routes` directory. 52 - 53 - TanStack will automatically generate the content of the route file for you. 54 - 55 - Now that you have two routes you can use a `Link` component to navigate between them. 56 - 57 - ### Adding Links 58 - 59 - To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`. 60 - 61 - ```tsx 62 - import { Link } from "@tanstack/react-router"; 14 + getCardById("abc-123") 15 + โ”‚ 16 + โ–ผ 17 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 18 + โ”‚ card LRU cache โ”‚โ”€โ”€hitโ”€โ”€โ–ถ return Card 19 + โ”‚ (10k cards) โ”‚ 20 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 21 + โ”‚ miss 22 + โ–ผ 23 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 24 + โ”‚ cards-byteindex โ”‚ binary search sorted UUIDs 25 + โ”‚ .bin โ”‚ 25 bytes/record: UUID(16) + chunk(1) + offset(4) + len(4) 26 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 27 + โ”‚ 28 + โ–ผ 29 + { chunk: 42, offset: 81920, len: 2048 } 30 + โ”‚ 31 + โ–ผ 32 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 33 + โ”‚ chunk LRU cache โ”‚โ”€โ”€hitโ”€โ”€โ–ถ use cached chunk text 34 + โ”‚ (12 chunks) โ”‚ 35 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 36 + โ”‚ miss 37 + โ–ผ 38 + fetch cards/cards-042-a1b2c3.json 39 + โ”‚ 40 + โ–ผ 41 + chunkText.slice(81920, 81920 + 2048) 42 + โ”‚ 43 + โ–ผ 44 + JSON.parse โ”€โ”€โ–ถ cache โ”€โ”€โ–ถ return Card 63 45 ``` 64 46 65 - Then anywhere in your JSX you can use it like so: 47 + On the client, a [SharedWorker](https://caniuse.com/sharedworkers) (or regular Worker on Android) loads everything into memory at startup: 66 48 67 - ```tsx 68 - <Link to="/about">About</Link> 69 49 ``` 70 - 71 - This will create a link that will navigate to the `/about` route. 72 - 73 - More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent). 74 - 75 - ### Using A Layout 76 - 77 - 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. 78 - 79 - Here is an example layout that includes a header: 80 - 81 - ```tsx 82 - import { Outlet, createRootRoute } from '@tanstack/react-router' 83 - import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' 84 - 85 - import { Link } from "@tanstack/react-router"; 86 - 87 - export const Route = createRootRoute({ 88 - component: () => ( 89 - <> 90 - <header> 91 - <nav> 92 - <Link to="/">Home</Link> 93 - <Link to="/about">About</Link> 94 - </nav> 95 - </header> 96 - <Outlet /> 97 - <TanStackRouterDevtools /> 98 - </> 99 - ), 100 - }) 101 - ``` 102 - 103 - The `<TanStackRouterDevtools />` component is not required so you can remove it if you don't want it in your layout. 104 - 105 - More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts). 106 - 107 - 108 - ## Data Fetching 109 - 110 - 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. 111 - 112 - For example: 113 - 114 - ```tsx 115 - const peopleRoute = createRoute({ 116 - getParentRoute: () => rootRoute, 117 - path: "/people", 118 - loader: async () => { 119 - const response = await fetch("https://swapi.dev/api/people"); 120 - return response.json() as Promise<{ 121 - results: { 122 - name: string; 123 - }[]; 124 - }>; 125 - }, 126 - component: () => { 127 - const data = peopleRoute.useLoaderData(); 128 - return ( 129 - <ul> 130 - {data.results.map((person) => ( 131 - <li key={person.name}>{person.name}</li> 132 - ))} 133 - </ul> 134 - ); 135 - }, 136 - }); 137 - ``` 138 - 139 - 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). 140 - 141 - ### React-Query 142 - 143 - React-Query is an excellent addition or alternative to route loading and integrating it into you application is a breeze. 144 - 145 - First add your dependencies: 146 - 147 - ```bash 148 - npm install @tanstack/react-query @tanstack/react-query-devtools 149 - ``` 150 - 151 - Next we'll need to create a query client and provider. We recommend putting those in `main.tsx`. 152 - 153 - ```tsx 154 - import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 155 - 156 - // ... 157 - 158 - const queryClient = new QueryClient(); 159 - 160 - // ... 161 - 162 - if (!rootElement.innerHTML) { 163 - const root = ReactDOM.createRoot(rootElement); 164 - 165 - root.render( 166 - <QueryClientProvider client={queryClient}> 167 - <RouterProvider router={router} /> 168 - </QueryClientProvider> 169 - ); 170 - } 171 - ``` 172 - 173 - You can also add TanStack Query Devtools to the root route (optional). 174 - 175 - ```tsx 176 - import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 177 - 178 - const rootRoute = createRootRoute({ 179 - component: () => ( 180 - <> 181 - <Outlet /> 182 - <ReactQueryDevtools buttonPosition="top-right" /> 183 - <TanStackRouterDevtools /> 184 - </> 185 - ), 186 - }); 50 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 51 + โ”‚ SharedWorker init โ”‚ 52 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 53 + โ”‚ 54 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 55 + โ–ผ โ–ผ โ–ผ 56 + fetch chunk 0 fetch chunk 1 ... fetch chunk N (parallel) 57 + โ”‚ โ”‚ โ”‚ 58 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 59 + โ–ผ 60 + merge into cards: Record<id, Card> 61 + โ”‚ 62 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 63 + โ–ผ โ–ผ โ–ผ 64 + build id index build oracle build MiniSearch 65 + Map<id, Card> โ†’ printings fuzzy index 66 + โ”‚ 67 + โ–ผ 68 + ~115k cards in memory 69 + ready for queries 187 70 ``` 188 71 189 - Now you can use `useQuery` to fetch your data. 190 - 191 - ```tsx 192 - import { useQuery } from "@tanstack/react-query"; 193 - 194 - import "./App.css"; 195 - 196 - function App() { 197 - const { data } = useQuery({ 198 - queryKey: ["people"], 199 - queryFn: () => 200 - fetch("https://swapi.dev/api/people") 201 - .then((res) => res.json()) 202 - .then((data) => data.results as { name: string }[]), 203 - initialData: [], 204 - }); 205 - 206 - return ( 207 - <div> 208 - <ul> 209 - {data.map((person) => ( 210 - <li key={person.name}>{person.name}</li> 211 - ))} 212 - </ul> 213 - </div> 214 - ); 215 - } 216 - 217 - export default App; 218 72 ``` 219 - 220 - 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). 221 - 222 - ## State Management 223 - 224 - 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. 225 - 226 - First you need to add TanStack Store as a dependency: 227 - 228 - ```bash 229 - npm install @tanstack/store 73 + searchCards("lightning bolt") 74 + โ”‚ 75 + โ–ผ 76 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 77 + โ”‚ main thread โ”‚ 78 + โ”‚ TanStack Query โ”€โ”€โ–ถ Comlink RPC call โ”‚ 79 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 80 + โ”‚ postMessage 81 + โ–ผ 82 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 83 + โ”‚ SharedWorker โ”‚ 84 + โ”‚ โ”‚ 85 + โ”‚ MiniSearch.search("lightning bolt") โ”‚ 86 + โ”‚ โ”‚ โ”‚ 87 + โ”‚ โ–ผ โ”‚ 88 + โ”‚ filter by restrictions (format, CI) โ”‚ 89 + โ”‚ โ”‚ โ”‚ 90 + โ”‚ โ–ผ โ”‚ 91 + โ”‚ return Card[] โ”‚ 92 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 93 + โ”‚ postMessage 94 + โ–ผ 95 + results hydrated in UI 230 96 ``` 231 97 232 - Now let's create a simple counter in the `src/App.tsx` file as a demonstration. 98 + Together, the two paths look like this: 233 99 234 - ```tsx 235 - import { useStore } from "@tanstack/react-store"; 236 - import { Store } from "@tanstack/store"; 237 - import "./App.css"; 238 - 239 - const countStore = new Store(0); 240 - 241 - function App() { 242 - const count = useStore(countStore); 243 - return ( 244 - <div> 245 - <button onClick={() => countStore.setState((n) => n + 1)}> 246 - Increment - {count} 247 - </button> 248 - </div> 249 - ); 250 - } 251 - 252 - export default App; 253 100 ``` 254 - 255 - 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. 256 - 257 - Let's check this out by doubling the count using derived state. 258 - 259 - ```tsx 260 - import { useStore } from "@tanstack/react-store"; 261 - import { Store, Derived } from "@tanstack/store"; 262 - import "./App.css"; 263 - 264 - const countStore = new Store(0); 265 - 266 - const doubledStore = new Derived({ 267 - fn: () => countStore.state * 2, 268 - deps: [countStore], 269 - }); 270 - doubledStore.mount(); 271 - 272 - function App() { 273 - const count = useStore(countStore); 274 - const doubledCount = useStore(doubledStore); 275 - 276 - return ( 277 - <div> 278 - <button onClick={() => countStore.setState((n) => n + 1)}> 279 - Increment - {count} 280 - </button> 281 - <div>Doubled - {doubledCount}</div> 282 - </div> 283 - ); 284 - } 285 - 286 - export default App; 101 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 102 + โ”‚ public/data/cards/ โ”‚ 103 + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 104 + โ”‚ โ”‚ cards-000-xxx.json โ”‚ โ”‚ 105 + โ”‚ โ”‚ cards-001-xxx.json โ”‚ โ”‚ 106 + โ”‚ โ”‚ ... โ”‚ โ”‚ 107 + โ”‚ โ”‚ cards-NNN-xxx.json โ”‚ โ”‚ 108 + โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ 109 + โ”‚ โ”‚ cards-byteindex.bin โ”‚ โ”‚ 110 + โ”‚ โ”‚ indexes.json โ”‚ โ”‚ 111 + โ”‚ โ”‚ volatile.bin โ”‚ โ”‚ 112 + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ 113 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 114 + โ”‚ โ”‚ 115 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 116 + โ”‚ SSR: binary search โ”‚ client: load all 117 + โ”‚ + byte slice โ”‚ into worker 118 + โ–ผ โ–ผ 119 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 120 + โ”‚ CF Worker (SSR) โ”‚ โ”‚ SharedWorker/Worker โ”‚ 121 + โ”‚ โ”‚ โ”‚ โ”‚ 122 + โ”‚ byteindex lookup O(logn)โ”‚ โ”‚ ~115k cards in RAM โ”‚ 123 + โ”‚ parse single card โ”‚ โ”‚ MiniSearch index โ”‚ 124 + โ”‚ LRU cache (cards+chunks)โ”‚ โ”‚ scryfall syntax engine โ”‚ 125 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 126 + โ”‚ โ”‚ 127 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 128 + โ–ผ 129 + TanStack Query cache unifies both 130 + (SSR preloads, client hydrates) 287 131 ``` 288 132 289 - 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. 133 + 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). 290 134 291 - 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. 135 + 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). 292 136 293 - 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). 137 + ## Further Reading 294 138 295 - # Demo files 139 + More detailed docs live in `.claude/` although they were written (by claude) to help claude keep track of the finer details of these systems: 296 140 297 - 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. 141 + - [CARD_DATA.md](./.claude/CARD_DATA.md) - card data pipeline and provider architecture 142 + - [SEARCH.md](./.claude/SEARCH.md) - scryfall-like query engine (lexer โ†’ parser โ†’ matcher) 143 + - [ATPROTO.md](./.claude/ATPROTO.md) - AT Protocol integration, PDS writes, Slingshot reads 144 + - [DECK_VALIDATION.md](./.claude/DECK_VALIDATION.md) - format rules with MTG comprehensive rules citations 145 + - [DECK_FORMATS.md](./.claude/DECK_FORMATS.md) - import/export format comparison (Arena, Moxfield, MTGO, etc.) 298 146 299 - # Learn More 147 + ## Beware! 300 148 301 - You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com). 149 + 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.