👁️
5
fork

Configure Feed

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

remove demo content, clean up

+496 -1177
+33 -33
.vscode/settings.json
··· 1 1 { 2 - "files.watcherExclude": { 3 - "**/routeTree.gen.ts": true 4 - }, 5 - "search.exclude": { 6 - "**/routeTree.gen.ts": true 7 - }, 8 - "files.readonlyInclude": { 9 - "**/routeTree.gen.ts": true 10 - }, 11 - "[javascript]": { 12 - "editor.defaultFormatter": "biomejs.biome" 13 - }, 14 - "[javascriptreact]": { 15 - "editor.defaultFormatter": "biomejs.biome" 16 - }, 17 - "[typescript]": { 18 - "editor.defaultFormatter": "biomejs.biome" 19 - }, 20 - "[typescriptreact]": { 21 - "editor.defaultFormatter": "biomejs.biome" 22 - }, 23 - "[json]": { 24 - "editor.defaultFormatter": "biomejs.biome" 25 - }, 26 - "[jsonc]": { 27 - "editor.defaultFormatter": "biomejs.biome" 28 - }, 29 - "[css]": { 30 - "editor.defaultFormatter": "biomejs.biome" 31 - }, 32 - "editor.codeActionsOnSave": { 33 - "source.organizeImports.biome": "explicit" 34 - } 2 + "files.watcherExclude": { 3 + "**/routeTree.gen.ts": true 4 + }, 5 + "search.exclude": { 6 + "**/routeTree.gen.ts": true 7 + }, 8 + "files.readonlyInclude": { 9 + "**/routeTree.gen.ts": true 10 + }, 11 + "[javascript]": { 12 + "editor.defaultFormatter": "biomejs.biome" 13 + }, 14 + "[javascriptreact]": { 15 + "editor.defaultFormatter": "biomejs.biome" 16 + }, 17 + "[typescript]": { 18 + "editor.defaultFormatter": "biomejs.biome" 19 + }, 20 + "[typescriptreact]": { 21 + "editor.defaultFormatter": "biomejs.biome" 22 + }, 23 + "[json]": { 24 + "editor.defaultFormatter": "biomejs.biome" 25 + }, 26 + "[jsonc]": { 27 + "editor.defaultFormatter": "biomejs.biome" 28 + }, 29 + "[css]": { 30 + "editor.defaultFormatter": "biomejs.biome" 31 + }, 32 + "editor.codeActionsOnSave": { 33 + "source.organizeImports.biome": "explicit" 34 + } 35 35 }
+2 -2
biome.json
··· 1 1 { 2 2 "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", 3 3 "vcs": { 4 - "enabled": false, 4 + "enabled": true, 5 5 "clientKind": "git", 6 - "useIgnoreFile": false 6 + "useIgnoreFile": true 7 7 }, 8 8 "files": { 9 9 "ignoreUnknown": false,
public/logo192.png

This is a binary file and will not be displayed.

public/logo512.png

This is a binary file and will not be displayed.

public/tanstack-circle-logo.png

This is a binary file and will not be displayed.

-1
public/tanstack-word-logo-white.svg
··· 1 - <svg height="660" viewBox="0 0 3178 660" width="3178" xmlns="http://www.w3.org/2000/svg"><g fill="#fff" transform="translate(.9778)"><g transform="translate(740.0222 38)"><path d="m101.695801 467h101.445312v-264.858398h90.917969v-80.390625h-283.28125v80.390625h90.917969z"/><path d="m241.544434 467h106.708984l68.666992-262.944336h33.017578v-82.304687h-95.703125zm70.820312-68.666992h211.025391l-21.054688-71.538086h-168.916015zm175.136719 68.666992h106.708984l-112.690429-345.249023h-62.685547v82.304687z"/><path d="m600.313965 467h101.445312v-179.443359h41.391602l-66.274414-38.759766 149.536133 218.203125h83.500976v-345.249023h-101.445312v176.572265h-41.391602l66.513672 38.759766-148.818359-215.332031h-84.458008z"/><path d="m1072.01318 473.220703c31.74154 0 58.85743-4.74528 81.34766-14.23584s39.67692-22.96875 51.56006-40.43457 17.82471-38.081869 17.82471-61.848145v-.239257c0-18.66211-3.94776-34.572754-11.84327-47.731934-7.8955-13.15918-19.89827-23.965658-36.0083-32.419434-16.11002-8.453776-36.52669-14.913737-61.25-19.379882l-34.69238-6.220703c-17.22656-3.190105-29.74772-6.898601-37.56348-11.125489-7.81575-4.226888-11.72363-10.248209-11.72363-18.063965v-.239257c0-5.263672 1.59505-10.008952 4.78516-14.23584 3.1901-4.226888 7.93538-7.576498 14.23584-10.048828 6.30045-2.472331 14.07633-3.708497 23.32763-3.708497 9.25131 0 17.5057 1.276042 24.76319 3.828126 7.25748 2.552083 13.07942 6.101074 17.46582 10.646972 4.38639 4.545899 6.8986 10.008952 7.53662 16.38916l.23926 2.392578h93.31054l-.23925-5.263671c-.95704-21.533204-7.01823-40.235189-18.1836-56.105957-11.16536-15.870769-27.27539-28.112793-48.33008-36.726075-21.05468-8.613281-46.97428-12.919922-77.75879-12.919922-27.27539 0-51.59993 4.625651-72.973628 13.876954-21.373698 9.251302-38.161621 22.330729-50.36377 39.238281-12.202148 16.907552-18.303222 36.925456-18.303222 60.053711v.239258c0 26.796875 9.131673 48.728841 27.395019 65.795898s44.541831 28.631185 78.835451 34.692383l34.69238 6.220703c19.14063 3.509115 32.61882 7.33724 40.43457 11.484375 7.81576 4.147135 11.72363 10.288086 11.72363 18.422852v.239257c0 5.742188-1.99381 10.846354-5.98144 15.3125s-9.61019 7.975261-16.86768 10.527344c-7.25748 2.552083-15.99039 3.828125-26.19873 3.828125-9.57031 0-18.3431-1.315918-26.31836-3.947754s-14.59472-6.260579-19.8584-10.88623c-5.26367-4.625651-8.61328-10.048828-10.04882-16.269532l-.47852-2.15332h-93.310546l.239258 4.545899c1.276042 22.649739 8.015137 41.909993 20.217285 57.780761 12.202149 15.870769 29.189453 27.953288 50.961914 36.247559 21.772459 8.294271 47.572429 12.441406 77.399899 12.441406z"/><path d="m1303.73682 467h101.44531v-264.858398h90.91797v-80.390625h-283.28125v80.390625h90.91797z"/><path d="m1443.58545 467h106.70898l68.667-262.944336h33.01757v-82.304687h-95.70312zm70.82031-68.666992h211.02539l-21.05469-71.538086h-168.91601zm175.13672 68.666992h106.70898l-112.69042-345.249023h-62.68555v82.304687z"/><path d="m1941.12451 473.220703c31.74154 0 59.65495-6.300456 83.74024-18.901367 24.08528-12.600912 42.94677-29.667969 56.58447-51.201172 13.63769-21.533203 20.45654-45.777995 20.45654-72.734375v-2.631836h-97.13867l-.23926 2.631836c-1.11653 12.122396-4.46614 22.689616-10.04883 31.70166-5.58268 9.012044-12.91992 15.990397-22.01171 20.935059-9.0918 4.944661-19.45964 7.416992-31.10352 7.416992-13.87695 0-25.9196-3.748372-36.12793-11.245117s-18.06396-18.462728-23.56689-32.897949c-5.50293-14.435222-8.2544-31.861166-8.2544-52.277832v-.239258c0-20.257162 2.75147-37.483724 8.2544-51.679688 5.50293-14.195963 13.31868-25.042317 23.44726-32.539062s22.13135-11.245117 36.0083-11.245117c12.60091 0 23.40739 2.591959 32.41944 7.775878 9.01204 5.18392 16.11002 12.281902 21.29394 21.293946s8.2544 19.260254 9.21143 30.744629l.23925 2.871093h97.13868v-2.15332c0-27.115885-6.69922-51.480306-20.09766-73.093262-13.39844-21.612955-32.10042-38.719889-56.10596-51.3208-24.00553-12.600912-52.03857-18.901368-84.09912-18.901368-35.09114 0-65.43701 6.978353-91.0376 20.935059-25.60058 13.956706-45.33935 34.213867-59.2163 60.771484-13.87696 26.557618-20.81543 58.817546-20.81543 96.779786v.239257c0 37.96224 6.8986 70.262045 20.6958 96.899414 13.7972 26.63737 33.49609 46.974284 59.09668 61.010743 25.60058 14.036458 56.0262 21.054687 91.27685 21.054687z"/><path d="m2214.23975 379.670898 75.36621-101.445312h26.0791l116.04004-156.474609h-106.46973l-106.70898 146.425781h-4.30664zm-99.05274 87.329102h101.44531v-345.249023h-101.44531zm203.84766 0h117.9541l-140.20508-226.577148-74.16992 64.121093z"/></g><path d="m305.114318.62443771c8.717817-1.14462121 17.926803-.36545135 26.712694-.36545135 32.548987 0 64.505987 5.05339923 95.64868 14.63098274 39.74418 12.2236582 76.762804 31.7666864 109.435876 57.477568 40.046637 31.5132839 73.228974 72.8472109 94.520714 119.2362609 39.836383 86.790386 39.544267 191.973146-1.268422 278.398081-26.388695 55.880442-68.724007 102.650458-119.964986 136.75724-41.808813 27.828603-90.706831 44.862601-140.45707 50.89341-63.325458 7.677926-131.784923-3.541603-188.712259-32.729444-106.868873-54.795293-179.52309291-165.076271-180.9604082-285.932068-.27660564-23.300971.08616998-46.74071 4.69884909-69.814998 7.51316071-37.57857 20.61272131-73.903917 40.28618971-106.877282 21.2814003-35.670293 48.7704861-67.1473767 81.6882804-92.5255597 38.602429-29.7610135 83.467691-51.1674988 130.978372-62.05777669 11.473831-2.62966514 22.9946-4.0869914 34.57273-5.4964306l3.658171-.44480576c3.050084-.37153079 6.104217-.74794222 9.162589-1.14972654zm-110.555861 549.44131429c-14.716752 1.577863-30.238964 4.25635-42.869928 12.522173 2.84343.683658 6.102369.004954 9.068638 0 7.124652-.011559 14.317732-.279903 21.434964.032202 17.817402.781913 36.381729 3.63214 53.58741 8.350042 22.029372 6.040631 41.432961 17.928687 62.656049 25.945156 22.389644 8.456554 44.67706 11.084675 68.427 11.084675 11.96813 0 23.845573-.035504 35.450133-3.302696-6.056202-3.225083-14.72582-2.619864-21.434964-3.963236-14.556814-2.915455-28.868774-6.474936-42.869928-11.470264-10.304996-3.676672-20.230803-8.214291-30.11097-12.848661l-6.348531-2.985046c-9.1705-4.309263-18.363277-8.560752-27.845391-12.142608-24.932161-9.418465-52.560181-14.071964-79.144482-11.221737zm22.259385-62.614168c-29.163917 0-58.660076 5.137344-84.915434 18.369597-6.361238 3.206092-12.407546 7.02566-18.137277 11.258891-1.746125 1.290529-4.841829 2.948483-5.487351 5.191839-.654591 2.275558 1.685942 4.182039 3.014086 5.637703 6.562396-3.497556 12.797498-7.199878 19.78612-9.855246 45.19892-17.169893 99.992458-13.570779 145.098218 2.172348 22.494346 7.851335 43.219483 19.592421 65.129314 28.800338 24.503461 10.297807 49.53043 16.975034 75.846795 20.399104 31.04195 4.037546 66.433549.7654 94.808495-13.242161 9.970556-4.921843 23.814245-12.422267 28.030337-23.320339-5.207047.454947-9.892236 2.685918-14.83959 4.224149-7.866632 2.445646-15.827248 4.51974-23.908229 6.138887-27.388113 5.486604-56.512458 6.619429-84.091013 1.639788-25.991939-4.693152-50.142596-14.119246-74.179513-24.03502l-3.068058-1.268177c-2.045137-.846788-4.089983-1.695816-6.135603-2.544467l-3.069142-1.272366c-12.279956-5.085721-24.606928-10.110797-37.210937-14.51024-24.485325-8.546552-50.726667-13.784628-76.671218-13.784628zm51.114145-447.9909432c-34.959602 7.7225298-66.276908 22.7605319-96.457338 41.7180089-17.521434 11.0054099-34.281927 22.2799893-49.465301 36.4444283-22.5792616 21.065423-39.8360564 46.668751-54.8866988 73.411509-15.507372 27.55357-25.4498976 59.665686-30.2554517 90.824149-4.7140432 30.568106-5.4906485 62.70747-.0906864 93.301172 6.7503648 38.248526 19.5989769 74.140579 39.8896436 107.337631 6.8187918-3.184625 11.659796-10.445603 17.3128555-15.336896 11.4149428-9.875888 23.3995608-19.029311 36.2745548-26.928535 4.765981-2.923712 9.662222-5.194315 14.83959-7.275014 1.953055-.785216 5.14604-1.502727 6.06527-3.647828 1.460876-3.406732-1.240754-9.335897-1.704904-12.865654-1.324845-10.095517-2.124534-20.362774-1.874735-30.549941.725492-29.668947 6.269727-59.751557 16.825623-87.521453 7.954845-20.924233 20.10682-39.922168 34.502872-56.971512 4.884699-5.785498 10.077731-11.170545 15.437296-16.512656 3.167428-3.157378 7.098271-5.858983 9.068639-9.908915-10.336599.006606-20.674847 2.987289-30.503603 6.013385-21.174447 6.519522-41.801477 16.19312-59.358362 29.841512-8.008432 6.226409-13.873368 14.387371-21.44733 20.939921-2.32322 2.010516-6.484901 4.704691-9.695199 3.187928-4.8500728-2.29042-4.1014979-11.835213-4.6571581-16.222019-2.1369011-16.873476 4.2548401-38.216325 12.3778671-52.843142 13.039878-23.479694 37.150915-43.528712 65.467327-42.82854 12.228647.302197 22.934587 4.551115 34.625711 7.324555-2.964621-4.211764-6.939158-7.28162-10.717482-10.733763-9.257431-8.459031-19.382979-16.184864-30.503603-22.028985-4.474136-2.350694-9.291232-3.77911-14.015169-5.506421-2.375159-.867783-5.36616-2.062533-6.259834-4.702213-1.654614-4.888817 7.148561-9.416813 10.381943-11.478522 12.499882-7.969406 27.826705-14.525258 42.869928-14.894334 23.509209-.577147 46.479246 12.467678 56.162903 34.665926 3.404469 7.803171 4.411273 16.054969 5.079109 24.382907l.121749 1.56229.174325 2.345587c.01913.260708.038244.521433.057403.782164l.11601 1.56437.120128 1.563971c7.38352-6.019164 12.576553-14.876995 19.78612-21.323859 16.861073-15.07846 39.936636-21.7722 61.831627-14.984333 19.786945 6.133107 36.984382 19.788105 47.105807 37.959541 2.648042 4.754231 10.035685 16.373942 4.698379 21.109183-4.177345 3.707277-9.475079.818243-13.880788-.719162-3.33605-1.16376-6.782939-1.90214-10.241828-2.585698l-1.887262-.369639c-.629089-.122886-1.257979-.246187-1.886079-.372129-11.980496-2.401886-25.91652-2.152533-37.923398-.041284-7.762754 1.364839-15.349083 4.127545-23.083807 5.271929v1.651348c21.149714.175043 41.608563 12.240618 52.043268 30.549941 4.323267 7.585468 6.482428 16.267431 8.138691 24.770223 2.047864 10.50918.608423 21.958802-2.263037 32.201289-.962925 3.433979-2.710699 9.255807-6.817143 10.046802-2.902789.558982-5.36781-2.330878-7.024898-4.279468-4.343878-5.10762-8.475879-9.96341-13.573278-14.374161-12.895604-11.157333-26.530715-21.449361-40.396663-31.373138-7.362086-5.269452-15.425755-12.12007-23.908229-15.340199 2.385052 5.745041 4.721463 11.086326 5.532694 17.339156 2.385876 18.392716-5.314223 35.704625-16.87179 49.540445-3.526876 4.222498-7.29943 8.475545-11.744712 11.755948-1.843407 1.360711-4.156734 3.137561-6.595373 2.752797-7.645687-1.207961-8.555849-12.73272-9.728176-18.637115-3.970415-19.998652-2.375984-39.861068 3.132802-59.448534-4.901187 2.485279-8.443727 7.923994-11.521293 12.385111-6.770975 9.816439-12.645804 20.199291-16.858599 31.375615-16.777806 44.519521-16.616219 96.664142 5.118834 139.523233 2.427098 4.786433 6.110614 4.144058 10.894733 4.144058.720854 0 1.44257-.004515 2.164851-.010924l2.168232-.022283c4.338648-.045438 8.686803-.064635 12.979772.508795 2.227588.297243 5.320818.032202 7.084256 1.673642 2.111344 1.966755.986008 5.338808.4996 7.758859-1.358647 6.765574-1.812904 12.914369-1.812904 19.816178 9.02412-1.398692 11.525415-15.866153 14.724172-23.118874 3.624982-8.216283 7.313444-16.440823 10.667192-24.770223 1.648843-4.093692 3.854171-8.671229 3.275427-13.210785-.649644-5.10184-4.335633-10.510831-6.904531-14.862134-4.86244-8.234447-10.389363-16.70834-13.969002-25.595896-2.861567-7.104926-.197036-15.983399 7.871579-18.521521 4.450228-1.400344 9.198073 1.345848 12.094266 4.562675 6.07269 6.74328 9.992815 16.777697 14.401823 24.692609l34.394873 61.925556c2.920926 5.243856 5.848447 10.481933 8.836976 15.687808 1.165732 2.031158 2.352075 5.167068 4.740424 6.0332 2.127008.77118 5.033095-.325315 7.148561-.748886 5.492297-1.099798 10.97635-2.287117 16.488434-3.28288 6.605266-1.193099 16.673928-.969342 21.434964-6.129805-6.963066-2.205375-15.011895-2.074919-22.259386-1.577863-4.352947.298894-9.178287 1.856116-13.178381-.686135-5.953149-3.783239-9.910373-12.522173-13.552668-18.377854-8.980425-14.439388-17.441465-29.095929-26.041008-43.760726l-1.376261-2.335014-2.765943-4.665258c-1.380597-2.334387-2.750786-4.67476-4.079753-7.036188-1.02723-1.826391-2.549937-4.233231-1.078344-6.24705 1.545791-2.114476 4.91472-2.239146 7.956473-2.243117l.603351.000261c1.195428.001526 2.315572.002427 3.222811-.11692 12.27399-1.615019 24.718635-2.952611 37.098976-2.952611-.963749-3.352237-3.719791-7.141255-2.838484-10.73046 1.972017-8.030506 13.526287-10.543033 18.899867-4.780653 3.60767 3.868283 5.704174 9.192229 8.051303 13.859765 3.097352 6.162006 6.624228 12.118418 9.940876 18.16483 5.805578 10.585967 12.146205 20.881297 18.116667 31.375615.49237.865561.999687 1.726685 1.512269 2.587098l.771613 1.290552c2.577138 4.303168 5.164895 8.635123 6.553094 13.461506-20.735854-.9487-36.30176-25.018751-45.343193-41.283704-.721369 2.604176.450959 4.928448 1.388326 7.431066 1.948109 5.197619 4.276275 10.147535 7.20627 14.862134 4.184765 6.732546 8.982075 13.665732 15.313633 18.553722 11.236043 8.673707 26.05255 8.721596 39.572241 7.794364 8.669619-.595311 19.50252-4.542034 28.030338-1.864372 8.513803 2.673532 11.940924 12.063098 6.884745 19.276187-3.787393 5.403211-8.842747 7.443452-15.128962 8.257566 4.445282 9.53571 10.268996 18.385285 14.490036 28.072919 1.758491 4.035895 3.59118 10.22102 7.8048 12.350433 2.805507 1.416857 6.824562.09743 9.85761.034678-3.043765-8.053625-8.742992-14.887729-11.541904-23.118874 8.533589.390544 16.786875 4.843404 24.732651 7.685374 15.630376 5.590144 31.063836 11.701854 46.475333 17.86913l7.112077 2.848685c6.338978 2.538947 12.71588 5.052299 18.961699 7.812528 2.285297 1.009799 5.449427 3.370401 7.975455 1.917215 2.061054-1.186494 3.394144-4.015253 4.665403-5.931643 3.55573-5.361927 6.775921-10.928622 9.965609-16.513481 12.774414-22.36586 22.143967-46.872692 28.402976-71.833646 20.645168-82.323009 2.934117-173.156241-46.677107-241.922507-19.061454-26.420745-43.033164-49.262193-69.46165-68.1783861-66.13923-47.336721-152.911262-66.294198-232.486917-48.7172481zm135.205158 410.5292842c-17.532977 4.570931-35.601827 8.714164-53.58741 11.040088 2.365265 8.052799 8.145286 15.885969 12.376218 23.118874 1.635653 2.796558 3.3859 6.541816 6.618457 7.755557 3.651364 1.370619 8.063669-.853747 11.508927-1.975838-1.595256-4.364513-4.279573-8.292245-6.476657-12.385112-.905215-1.687677-2.305907-3.685809-1.559805-5.68972 1.410585-3.786541 7.266452-3.563609 10.509727-4.221671 8.54678-1.733916 17.004522-3.898008 25.557073-5.611281 3.150939-.631641 7.538512-2.342438 10.705115-1.285575 2.371037.791232 3.800147 2.744743 5.152304 4.781948l.606196.918752c.80912 1.222827 1.637246 2.41754 2.671212 3.351165 3.457625 3.121874 8.628398 3.60159 13.017619 4.453686-2.678546-6.027421-7.130424-11.301001-9.984571-17.339156-1.659561-3.511592-3.023155-8.677834-6.656381-10.707341-5.005064-2.795733-15.341663 2.461334-20.458024 3.795624zm-110.472507-40.151706c-.825246 10.467897-4.036369 18.984725-9.068639 28.072919 5.76683.729896 11.649079.989984 17.312856 2.39363 4.244947 1.051908 8.156828 3.058296 12.366325 4.211763-2.250671-6.157877-6.426367-11.651913-9.661398-17.339156-3.266358-5.740912-6.189758-12.717032-10.949144-17.339156z"/></g></svg>
+43 -1
scripts/download-scryfall.ts
··· 6 6 * Fetches: 7 7 * - default_cards bulk data (all English cards) 8 8 * - migrations data (UUID changes) 9 + * - mana symbol SVGs 9 10 * 10 11 * Outputs: 11 12 * - public/data/cards.json - filtered card data with indexes 12 13 * - public/data/migrations.json - ID migration mappings 14 + * - public/symbols/*.svg - mana symbol images 13 15 */ 14 16 15 17 import { writeFile, mkdir, readFile, stat } from "node:fs/promises"; ··· 21 23 const __filename = fileURLToPath(import.meta.url); 22 24 const __dirname = dirname(__filename); 23 25 const OUTPUT_DIR = join(__dirname, "../public/data"); 26 + const SYMBOLS_DIR = join(__dirname, "../public/symbols"); 24 27 const TEMP_DIR = join(__dirname, "../.cache"); 25 28 26 29 // Fields to keep from Scryfall data ··· 120 123 data: Migration[]; 121 124 } 122 125 126 + interface CardSymbol { 127 + object: string; 128 + symbol: string; 129 + svg_uri: string; 130 + english: string; 131 + represents_mana: boolean; 132 + appears_in_mana_costs: boolean; 133 + } 134 + 135 + interface SymbologyResponse { 136 + data: CardSymbol[]; 137 + } 138 + 123 139 type MigrationMap = Record<string, string>; 124 140 125 141 async function fetchJSON<T>(url: string): Promise<T> { ··· 243 259 return migrationMap; 244 260 } 245 261 262 + async function downloadSymbols(): Promise<number> { 263 + console.log("Fetching symbology..."); 264 + const symbology = await fetchJSON<SymbologyResponse>( 265 + "https://api.scryfall.com/symbology", 266 + ); 267 + 268 + console.log(`Found ${symbology.data.length} symbols`); 269 + 270 + await mkdir(SYMBOLS_DIR, { recursive: true }); 271 + 272 + let downloaded = 0; 273 + for (const symbol of symbology.data) { 274 + // Extract filename from symbol (e.g., "{T}" -> "T") 275 + const filename = symbol.symbol.replace(/[{}]/g, "").toLowerCase(); 276 + const outputPath = join(SYMBOLS_DIR, `${filename}.svg`); 277 + 278 + await downloadFile(symbol.svg_uri, outputPath); 279 + downloaded++; 280 + } 281 + 282 + console.log(`Downloaded ${downloaded} symbol SVGs to: ${SYMBOLS_DIR}`); 283 + return downloaded; 284 + } 285 + 246 286 async function main(): Promise<void> { 247 287 try { 248 288 console.log("=== Scryfall Data Download ===\n"); 249 289 250 - const [cardsData, migrations] = await Promise.all([ 290 + const [cardsData, migrations, symbolCount] = await Promise.all([ 251 291 processBulkData(), 252 292 processMigrations(), 293 + downloadSymbols(), 253 294 ]); 254 295 255 296 console.log("\n=== Summary ==="); 256 297 console.log(`Cards: ${cardsData.cardCount.toLocaleString()}`); 257 298 console.log(`Migrations: ${Object.keys(migrations).length.toLocaleString()}`); 299 + console.log(`Symbols: ${symbolCount.toLocaleString()}`); 258 300 console.log(`Version: ${cardsData.version}`); 259 301 console.log("\n✓ Done!"); 260 302 } catch (error) {
+128
src/components/CardImage.tsx
··· 1 + /** 2 + * Reusable card image components for different use cases 3 + */ 4 + 5 + import type { Card, ScryfallId } from "../lib/scryfall-types"; 6 + import { getImageUri } from "../lib/scryfall-utils"; 7 + 8 + interface CardImageProps { 9 + card: Card; 10 + size?: "small" | "normal" | "large"; 11 + className?: string; 12 + } 13 + 14 + /** 15 + * Basic card image without container 16 + */ 17 + export function CardImage({ 18 + card, 19 + size = "normal", 20 + className, 21 + }: CardImageProps) { 22 + return ( 23 + <img 24 + src={getImageUri(card.id, size)} 25 + alt={card.name} 26 + className={`${className ?? ""}`} 27 + loading="lazy" 28 + /> 29 + ); 30 + } 31 + 32 + interface CardThumbnailProps { 33 + card: Card; 34 + href?: string; 35 + onClick?: () => void; 36 + } 37 + 38 + /** 39 + * Card thumbnail with hover effects and optional link/click 40 + */ 41 + export function CardThumbnail({ card, href, onClick }: CardThumbnailProps) { 42 + const content = ( 43 + <> 44 + <CardImage 45 + card={card} 46 + size="small" 47 + className="w-full h-full object-cover" 48 + /> 49 + <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/0 to-black/0 opacity-0 group-hover:opacity-100 transition-opacity"> 50 + <div className="absolute bottom-0 left-0 right-0 p-3"> 51 + <p className="text-white font-semibold text-sm line-clamp-2"> 52 + {card.name} 53 + </p> 54 + {card.set_name && ( 55 + <p className="text-gray-300 text-xs mt-1">{card.set_name}</p> 56 + )} 57 + </div> 58 + </div> 59 + </> 60 + ); 61 + 62 + const className = 63 + "group relative aspect-[5/7] rounded-lg overflow-hidden bg-slate-800 hover:ring-2 hover:ring-cyan-500 transition-all block"; 64 + 65 + if (href) { 66 + return ( 67 + <a href={href} className={className}> 68 + {content} 69 + </a> 70 + ); 71 + } 72 + 73 + if (onClick) { 74 + return ( 75 + <button type="button" onClick={onClick} className={className}> 76 + {content} 77 + </button> 78 + ); 79 + } 80 + 81 + return <div className={className}>{content}</div>; 82 + } 83 + 84 + interface CardPreviewProps { 85 + cardId: ScryfallId; 86 + name: string; 87 + setName?: string; 88 + href?: string; 89 + className?: string; 90 + } 91 + 92 + /** 93 + * Minimal card preview (for "other printings" grids) 94 + */ 95 + export function CardPreview({ 96 + cardId, 97 + name, 98 + setName, 99 + href, 100 + className, 101 + }: CardPreviewProps) { 102 + const content = ( 103 + <img 104 + src={getImageUri(cardId, "small")} 105 + alt={name} 106 + className="w-full h-full object-cover" 107 + loading="lazy" 108 + /> 109 + ); 110 + 111 + const baseClassName = 112 + "aspect-[5/7] rounded overflow-hidden hover:ring-2 hover:ring-cyan-500 transition-all block"; 113 + const finalClassName = `${baseClassName} ${className ?? ""}`; 114 + 115 + if (href) { 116 + return ( 117 + <a href={href} className={finalClassName} title={setName}> 118 + {content} 119 + </a> 120 + ); 121 + } 122 + 123 + return ( 124 + <div className={finalClassName} title={setName}> 125 + {content} 126 + </div> 127 + ); 128 + }
+35
src/components/CardSymbol.tsx
··· 1 + /** 2 + * Renders a single MTG card symbol (mana, tap, energy, etc) 3 + * 4 + * Uses locally downloaded symbol SVGs from Scryfall. 5 + */ 6 + 7 + interface CardSymbolProps { 8 + symbol: string; 9 + size?: "small" | "medium" | "large"; 10 + className?: string; 11 + } 12 + 13 + const SIZE_CLASSES = { 14 + small: "w-4 h-4", 15 + medium: "w-5 h-5", 16 + large: "w-6 h-6", 17 + }; 18 + 19 + export function CardSymbol({ 20 + symbol, 21 + size = "medium", 22 + className, 23 + }: CardSymbolProps) { 24 + // Normalize symbol for filename (e.g., "2/W" -> "2w", "T" -> "t") 25 + const normalizedSymbol = symbol.toLowerCase().replace("/", ""); 26 + 27 + return ( 28 + <img 29 + src={`/symbols/${normalizedSymbol}.svg`} 30 + alt={symbol} 31 + className={`${SIZE_CLASSES[size]} ${className ?? ""}`} 32 + title={symbol} 33 + /> 34 + ); 35 + }
+64 -183
src/components/Header.tsx
··· 1 - import { Link } from '@tanstack/react-router' 2 - 3 - import { useState } from 'react' 4 - import { 5 - ChevronDown, 6 - ChevronRight, 7 - Home, 8 - Menu, 9 - Network, 10 - SquareFunction, 11 - StickyNote, 12 - X, 13 - } from 'lucide-react' 1 + import { Link } from "@tanstack/react-router"; 2 + import { Home, Library, Menu, X } from "lucide-react"; 3 + import { useState } from "react"; 14 4 15 5 export default function Header() { 16 - const [isOpen, setIsOpen] = useState(false) 17 - const [groupedExpanded, setGroupedExpanded] = useState< 18 - Record<string, boolean> 19 - >({}) 6 + const [isOpen, setIsOpen] = useState(false); 20 7 21 - return ( 22 - <> 23 - <header className="p-4 flex items-center bg-gray-800 text-white shadow-lg"> 24 - <button 25 - onClick={() => setIsOpen(true)} 26 - className="p-2 hover:bg-gray-700 rounded-lg transition-colors" 27 - aria-label="Open menu" 28 - > 29 - <Menu size={24} /> 30 - </button> 31 - <h1 className="ml-4 text-xl font-semibold"> 32 - <Link to="/"> 33 - <img 34 - src="/tanstack-word-logo-white.svg" 35 - alt="TanStack Logo" 36 - className="h-10" 37 - /> 38 - </Link> 39 - </h1> 40 - </header> 8 + return ( 9 + <> 10 + <header className="p-4 flex items-center bg-gray-800 text-white shadow-lg"> 11 + <button 12 + type="button" 13 + onClick={() => setIsOpen(true)} 14 + className="p-2 hover:bg-gray-700 rounded-lg transition-colors" 15 + aria-label="Open menu" 16 + > 17 + <Menu size={24} /> 18 + </button> 19 + <h1 className="ml-4 text-xl font-semibold"> 20 + <Link to="/">DeckBelcher</Link> 21 + </h1> 22 + </header> 41 23 42 - <aside 43 - className={`fixed top-0 left-0 h-full w-80 bg-gray-900 text-white shadow-2xl z-50 transform transition-transform duration-300 ease-in-out flex flex-col ${ 44 - isOpen ? 'translate-x-0' : '-translate-x-full' 45 - }`} 46 - > 47 - <div className="flex items-center justify-between p-4 border-b border-gray-700"> 48 - <h2 className="text-xl font-bold">Navigation</h2> 49 - <button 50 - onClick={() => setIsOpen(false)} 51 - className="p-2 hover:bg-gray-800 rounded-lg transition-colors" 52 - aria-label="Close menu" 53 - > 54 - <X size={24} /> 55 - </button> 56 - </div> 24 + <aside 25 + className={`fixed top-0 left-0 h-full w-80 bg-gray-900 text-white shadow-2xl z-50 transform transition-transform duration-300 ease-in-out flex flex-col ${ 26 + isOpen ? "translate-x-0" : "-translate-x-full" 27 + }`} 28 + > 29 + <div className="flex items-center justify-between p-4 border-b border-gray-700"> 30 + <h2 className="text-xl font-bold">Navigation</h2> 31 + <button 32 + type="button" 33 + onClick={() => setIsOpen(false)} 34 + className="p-2 hover:bg-gray-800 rounded-lg transition-colors" 35 + aria-label="Close menu" 36 + > 37 + <X size={24} /> 38 + </button> 39 + </div> 57 40 58 - <nav className="flex-1 p-4 overflow-y-auto"> 59 - <Link 60 - to="/" 61 - onClick={() => setIsOpen(false)} 62 - className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2" 63 - activeProps={{ 64 - className: 65 - 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2', 66 - }} 67 - > 68 - <Home size={20} /> 69 - <span className="font-medium">Home</span> 70 - </Link> 41 + <nav className="flex-1 p-4 overflow-y-auto"> 42 + <Link 43 + to="/" 44 + onClick={() => setIsOpen(false)} 45 + className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2" 46 + activeProps={{ 47 + className: 48 + "flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2", 49 + }} 50 + > 51 + <Home size={20} /> 52 + <span className="font-medium">Home</span> 53 + </Link> 71 54 72 - {/* Demo Links Start */} 73 - 74 - <Link 75 - to="/demo/start/server-funcs" 76 - onClick={() => setIsOpen(false)} 77 - className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2" 78 - activeProps={{ 79 - className: 80 - 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2', 81 - }} 82 - > 83 - <SquareFunction size={20} /> 84 - <span className="font-medium">Start - Server Functions</span> 85 - </Link> 86 - 87 - <Link 88 - to="/demo/start/api-request" 89 - onClick={() => setIsOpen(false)} 90 - className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2" 91 - activeProps={{ 92 - className: 93 - 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2', 94 - }} 95 - > 96 - <Network size={20} /> 97 - <span className="font-medium">Start - API Request</span> 98 - </Link> 99 - 100 - <div className="flex flex-row justify-between"> 101 - <Link 102 - to="/demo/start/ssr" 103 - onClick={() => setIsOpen(false)} 104 - className="flex-1 flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2" 105 - activeProps={{ 106 - className: 107 - 'flex-1 flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2', 108 - }} 109 - > 110 - <StickyNote size={20} /> 111 - <span className="font-medium">Start - SSR Demos</span> 112 - </Link> 113 - <button 114 - className="p-2 hover:bg-gray-800 rounded-lg transition-colors" 115 - onClick={() => 116 - setGroupedExpanded((prev) => ({ 117 - ...prev, 118 - StartSSRDemo: !prev.StartSSRDemo, 119 - })) 120 - } 121 - > 122 - {groupedExpanded.StartSSRDemo ? ( 123 - <ChevronDown size={20} /> 124 - ) : ( 125 - <ChevronRight size={20} /> 126 - )} 127 - </button> 128 - </div> 129 - {groupedExpanded.StartSSRDemo && ( 130 - <div className="flex flex-col ml-4"> 131 - <Link 132 - to="/demo/start/ssr/spa-mode" 133 - onClick={() => setIsOpen(false)} 134 - className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2" 135 - activeProps={{ 136 - className: 137 - 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2', 138 - }} 139 - > 140 - <StickyNote size={20} /> 141 - <span className="font-medium">SPA Mode</span> 142 - </Link> 143 - 144 - <Link 145 - to="/demo/start/ssr/full-ssr" 146 - onClick={() => setIsOpen(false)} 147 - className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2" 148 - activeProps={{ 149 - className: 150 - 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2', 151 - }} 152 - > 153 - <StickyNote size={20} /> 154 - <span className="font-medium">Full SSR</span> 155 - </Link> 156 - 157 - <Link 158 - to="/demo/start/ssr/data-only" 159 - onClick={() => setIsOpen(false)} 160 - className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2" 161 - activeProps={{ 162 - className: 163 - 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2', 164 - }} 165 - > 166 - <StickyNote size={20} /> 167 - <span className="font-medium">Data Only</span> 168 - </Link> 169 - </div> 170 - )} 171 - 172 - <Link 173 - to="/demo/tanstack-query" 174 - onClick={() => setIsOpen(false)} 175 - className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2" 176 - activeProps={{ 177 - className: 178 - 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2', 179 - }} 180 - > 181 - <Network size={20} /> 182 - <span className="font-medium">TanStack Query</span> 183 - </Link> 184 - 185 - {/* Demo Links End */} 186 - </nav> 187 - </aside> 188 - </> 189 - ) 55 + <Link 56 + to="/cards" 57 + onClick={() => setIsOpen(false)} 58 + className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2" 59 + activeProps={{ 60 + className: 61 + "flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2", 62 + }} 63 + > 64 + <Library size={20} /> 65 + <span className="font-medium">Card Browser</span> 66 + </Link> 67 + </nav> 68 + </aside> 69 + </> 70 + ); 190 71 }
+37
src/components/ManaCost.tsx
··· 1 + /** 2 + * Renders MTG mana cost 3 + * 4 + * Parses mana cost strings like "{2}{U}{B}" and renders them as symbols. 5 + */ 6 + 7 + import { CardSymbol } from "./CardSymbol"; 8 + 9 + interface ManaCostProps { 10 + cost: string; 11 + size?: "small" | "medium" | "large"; 12 + } 13 + 14 + export function ManaCost({ cost, size = "medium" }: ManaCostProps) { 15 + // Parse mana cost string like "{2}{U}{B}" into symbols 16 + const symbols = Array.from( 17 + cost.matchAll(/\{([^}]+)\}/g), 18 + (match) => match[1], 19 + ); 20 + 21 + if (symbols.length === 0) { 22 + return null; 23 + } 24 + 25 + return ( 26 + <div className="inline-flex items-center gap-0.5"> 27 + {symbols.map((symbol, i) => ( 28 + <CardSymbol 29 + // biome-ignore lint/suspicious/noArrayIndexKey: mana symbols are stable ordered list 30 + key={i} 31 + symbol={symbol} 32 + size={size} 33 + /> 34 + ))} 35 + </div> 36 + ); 37 + }
-13
src/data/demo.punk-songs.ts
··· 1 - import { createServerFn } from '@tanstack/react-start' 2 - 3 - export const getPunkSongs = createServerFn({ 4 - method: 'GET', 5 - }).handler(async () => [ 6 - { id: 1, name: 'Teenage Dirtbag', artist: 'Wheatus' }, 7 - { id: 2, name: 'Smells Like Teen Spirit', artist: 'Nirvana' }, 8 - { id: 3, name: 'The Middle', artist: 'Jimmy Eat World' }, 9 - { id: 4, name: 'My Own Worst Enemy', artist: 'Lit' }, 10 - { id: 5, name: 'Fat Lip', artist: 'Sum 41' }, 11 - { id: 6, name: 'All the Small Things', artist: 'blink-182' }, 12 - { id: 7, name: 'Beverly Hills', artist: 'Weezer' }, 13 - ])
+4 -4
src/integrations/tanstack-query/devtools.tsx
··· 1 - import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' 1 + import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; 2 2 3 3 export default { 4 - name: 'Tanstack Query', 5 - render: <ReactQueryDevtoolsPanel />, 6 - } 4 + name: "Tanstack Query", 5 + render: <ReactQueryDevtoolsPanel />, 6 + };
+12 -12
src/integrations/tanstack-query/root-provider.tsx
··· 1 - import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 1 + import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 2 3 3 export function getContext() { 4 - const queryClient = new QueryClient() 5 - return { 6 - queryClient, 7 - } 4 + const queryClient = new QueryClient(); 5 + return { 6 + queryClient, 7 + }; 8 8 } 9 9 10 10 export function Provider({ 11 - children, 12 - queryClient, 11 + children, 12 + queryClient, 13 13 }: { 14 - children: React.ReactNode 15 - queryClient: QueryClient 14 + children: React.ReactNode; 15 + queryClient: QueryClient; 16 16 }) { 17 - return ( 18 - <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> 19 - ) 17 + return ( 18 + <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> 19 + ); 20 20 }
+3 -202
src/routeTree.gen.ts
··· 11 11 import { Route as rootRouteImport } from './routes/__root' 12 12 import { Route as IndexRouteImport } from './routes/index' 13 13 import { Route as CardsIndexRouteImport } from './routes/cards/index' 14 - import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query' 15 14 import { Route as CardsIdRouteImport } from './routes/cards/$id' 16 - import { Route as DemoStartServerFuncsRouteImport } from './routes/demo/start.server-funcs' 17 - import { Route as DemoStartApiRequestRouteImport } from './routes/demo/start.api-request' 18 - import { Route as DemoApiTqTodosRouteImport } from './routes/demo/api.tq-todos' 19 - import { Route as DemoApiNamesRouteImport } from './routes/demo/api.names' 20 - import { Route as DemoStartSsrIndexRouteImport } from './routes/demo/start.ssr.index' 21 - import { Route as DemoStartSsrSpaModeRouteImport } from './routes/demo/start.ssr.spa-mode' 22 - import { Route as DemoStartSsrFullSsrRouteImport } from './routes/demo/start.ssr.full-ssr' 23 - import { Route as DemoStartSsrDataOnlyRouteImport } from './routes/demo/start.ssr.data-only' 24 15 25 16 const IndexRoute = IndexRouteImport.update({ 26 17 id: '/', ··· 32 23 path: '/cards/', 33 24 getParentRoute: () => rootRouteImport, 34 25 } as any) 35 - const DemoTanstackQueryRoute = DemoTanstackQueryRouteImport.update({ 36 - id: '/demo/tanstack-query', 37 - path: '/demo/tanstack-query', 38 - getParentRoute: () => rootRouteImport, 39 - } as any) 40 26 const CardsIdRoute = CardsIdRouteImport.update({ 41 27 id: '/cards/$id', 42 28 path: '/cards/$id', 43 29 getParentRoute: () => rootRouteImport, 44 30 } as any) 45 - const DemoStartServerFuncsRoute = DemoStartServerFuncsRouteImport.update({ 46 - id: '/demo/start/server-funcs', 47 - path: '/demo/start/server-funcs', 48 - getParentRoute: () => rootRouteImport, 49 - } as any) 50 - const DemoStartApiRequestRoute = DemoStartApiRequestRouteImport.update({ 51 - id: '/demo/start/api-request', 52 - path: '/demo/start/api-request', 53 - getParentRoute: () => rootRouteImport, 54 - } as any) 55 - const DemoApiTqTodosRoute = DemoApiTqTodosRouteImport.update({ 56 - id: '/demo/api/tq-todos', 57 - path: '/demo/api/tq-todos', 58 - getParentRoute: () => rootRouteImport, 59 - } as any) 60 - const DemoApiNamesRoute = DemoApiNamesRouteImport.update({ 61 - id: '/demo/api/names', 62 - path: '/demo/api/names', 63 - getParentRoute: () => rootRouteImport, 64 - } as any) 65 - const DemoStartSsrIndexRoute = DemoStartSsrIndexRouteImport.update({ 66 - id: '/demo/start/ssr/', 67 - path: '/demo/start/ssr/', 68 - getParentRoute: () => rootRouteImport, 69 - } as any) 70 - const DemoStartSsrSpaModeRoute = DemoStartSsrSpaModeRouteImport.update({ 71 - id: '/demo/start/ssr/spa-mode', 72 - path: '/demo/start/ssr/spa-mode', 73 - getParentRoute: () => rootRouteImport, 74 - } as any) 75 - const DemoStartSsrFullSsrRoute = DemoStartSsrFullSsrRouteImport.update({ 76 - id: '/demo/start/ssr/full-ssr', 77 - path: '/demo/start/ssr/full-ssr', 78 - getParentRoute: () => rootRouteImport, 79 - } as any) 80 - const DemoStartSsrDataOnlyRoute = DemoStartSsrDataOnlyRouteImport.update({ 81 - id: '/demo/start/ssr/data-only', 82 - path: '/demo/start/ssr/data-only', 83 - getParentRoute: () => rootRouteImport, 84 - } as any) 85 31 86 32 export interface FileRoutesByFullPath { 87 33 '/': typeof IndexRoute 88 34 '/cards/$id': typeof CardsIdRoute 89 - '/demo/tanstack-query': typeof DemoTanstackQueryRoute 90 35 '/cards': typeof CardsIndexRoute 91 - '/demo/api/names': typeof DemoApiNamesRoute 92 - '/demo/api/tq-todos': typeof DemoApiTqTodosRoute 93 - '/demo/start/api-request': typeof DemoStartApiRequestRoute 94 - '/demo/start/server-funcs': typeof DemoStartServerFuncsRoute 95 - '/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute 96 - '/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute 97 - '/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute 98 - '/demo/start/ssr': typeof DemoStartSsrIndexRoute 99 36 } 100 37 export interface FileRoutesByTo { 101 38 '/': typeof IndexRoute 102 39 '/cards/$id': typeof CardsIdRoute 103 - '/demo/tanstack-query': typeof DemoTanstackQueryRoute 104 40 '/cards': typeof CardsIndexRoute 105 - '/demo/api/names': typeof DemoApiNamesRoute 106 - '/demo/api/tq-todos': typeof DemoApiTqTodosRoute 107 - '/demo/start/api-request': typeof DemoStartApiRequestRoute 108 - '/demo/start/server-funcs': typeof DemoStartServerFuncsRoute 109 - '/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute 110 - '/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute 111 - '/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute 112 - '/demo/start/ssr': typeof DemoStartSsrIndexRoute 113 41 } 114 42 export interface FileRoutesById { 115 43 __root__: typeof rootRouteImport 116 44 '/': typeof IndexRoute 117 45 '/cards/$id': typeof CardsIdRoute 118 - '/demo/tanstack-query': typeof DemoTanstackQueryRoute 119 46 '/cards/': typeof CardsIndexRoute 120 - '/demo/api/names': typeof DemoApiNamesRoute 121 - '/demo/api/tq-todos': typeof DemoApiTqTodosRoute 122 - '/demo/start/api-request': typeof DemoStartApiRequestRoute 123 - '/demo/start/server-funcs': typeof DemoStartServerFuncsRoute 124 - '/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute 125 - '/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute 126 - '/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute 127 - '/demo/start/ssr/': typeof DemoStartSsrIndexRoute 128 47 } 129 48 export interface FileRouteTypes { 130 49 fileRoutesByFullPath: FileRoutesByFullPath 131 - fullPaths: 132 - | '/' 133 - | '/cards/$id' 134 - | '/demo/tanstack-query' 135 - | '/cards' 136 - | '/demo/api/names' 137 - | '/demo/api/tq-todos' 138 - | '/demo/start/api-request' 139 - | '/demo/start/server-funcs' 140 - | '/demo/start/ssr/data-only' 141 - | '/demo/start/ssr/full-ssr' 142 - | '/demo/start/ssr/spa-mode' 143 - | '/demo/start/ssr' 50 + fullPaths: '/' | '/cards/$id' | '/cards' 144 51 fileRoutesByTo: FileRoutesByTo 145 - to: 146 - | '/' 147 - | '/cards/$id' 148 - | '/demo/tanstack-query' 149 - | '/cards' 150 - | '/demo/api/names' 151 - | '/demo/api/tq-todos' 152 - | '/demo/start/api-request' 153 - | '/demo/start/server-funcs' 154 - | '/demo/start/ssr/data-only' 155 - | '/demo/start/ssr/full-ssr' 156 - | '/demo/start/ssr/spa-mode' 157 - | '/demo/start/ssr' 158 - id: 159 - | '__root__' 160 - | '/' 161 - | '/cards/$id' 162 - | '/demo/tanstack-query' 163 - | '/cards/' 164 - | '/demo/api/names' 165 - | '/demo/api/tq-todos' 166 - | '/demo/start/api-request' 167 - | '/demo/start/server-funcs' 168 - | '/demo/start/ssr/data-only' 169 - | '/demo/start/ssr/full-ssr' 170 - | '/demo/start/ssr/spa-mode' 171 - | '/demo/start/ssr/' 52 + to: '/' | '/cards/$id' | '/cards' 53 + id: '__root__' | '/' | '/cards/$id' | '/cards/' 172 54 fileRoutesById: FileRoutesById 173 55 } 174 56 export interface RootRouteChildren { 175 57 IndexRoute: typeof IndexRoute 176 58 CardsIdRoute: typeof CardsIdRoute 177 - DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute 178 59 CardsIndexRoute: typeof CardsIndexRoute 179 - DemoApiNamesRoute: typeof DemoApiNamesRoute 180 - DemoApiTqTodosRoute: typeof DemoApiTqTodosRoute 181 - DemoStartApiRequestRoute: typeof DemoStartApiRequestRoute 182 - DemoStartServerFuncsRoute: typeof DemoStartServerFuncsRoute 183 - DemoStartSsrDataOnlyRoute: typeof DemoStartSsrDataOnlyRoute 184 - DemoStartSsrFullSsrRoute: typeof DemoStartSsrFullSsrRoute 185 - DemoStartSsrSpaModeRoute: typeof DemoStartSsrSpaModeRoute 186 - DemoStartSsrIndexRoute: typeof DemoStartSsrIndexRoute 187 60 } 188 61 189 62 declare module '@tanstack/react-router' { ··· 202 75 preLoaderRoute: typeof CardsIndexRouteImport 203 76 parentRoute: typeof rootRouteImport 204 77 } 205 - '/demo/tanstack-query': { 206 - id: '/demo/tanstack-query' 207 - path: '/demo/tanstack-query' 208 - fullPath: '/demo/tanstack-query' 209 - preLoaderRoute: typeof DemoTanstackQueryRouteImport 210 - parentRoute: typeof rootRouteImport 211 - } 212 78 '/cards/$id': { 213 79 id: '/cards/$id' 214 80 path: '/cards/$id' ··· 216 82 preLoaderRoute: typeof CardsIdRouteImport 217 83 parentRoute: typeof rootRouteImport 218 84 } 219 - '/demo/start/server-funcs': { 220 - id: '/demo/start/server-funcs' 221 - path: '/demo/start/server-funcs' 222 - fullPath: '/demo/start/server-funcs' 223 - preLoaderRoute: typeof DemoStartServerFuncsRouteImport 224 - parentRoute: typeof rootRouteImport 225 - } 226 - '/demo/start/api-request': { 227 - id: '/demo/start/api-request' 228 - path: '/demo/start/api-request' 229 - fullPath: '/demo/start/api-request' 230 - preLoaderRoute: typeof DemoStartApiRequestRouteImport 231 - parentRoute: typeof rootRouteImport 232 - } 233 - '/demo/api/tq-todos': { 234 - id: '/demo/api/tq-todos' 235 - path: '/demo/api/tq-todos' 236 - fullPath: '/demo/api/tq-todos' 237 - preLoaderRoute: typeof DemoApiTqTodosRouteImport 238 - parentRoute: typeof rootRouteImport 239 - } 240 - '/demo/api/names': { 241 - id: '/demo/api/names' 242 - path: '/demo/api/names' 243 - fullPath: '/demo/api/names' 244 - preLoaderRoute: typeof DemoApiNamesRouteImport 245 - parentRoute: typeof rootRouteImport 246 - } 247 - '/demo/start/ssr/': { 248 - id: '/demo/start/ssr/' 249 - path: '/demo/start/ssr' 250 - fullPath: '/demo/start/ssr' 251 - preLoaderRoute: typeof DemoStartSsrIndexRouteImport 252 - parentRoute: typeof rootRouteImport 253 - } 254 - '/demo/start/ssr/spa-mode': { 255 - id: '/demo/start/ssr/spa-mode' 256 - path: '/demo/start/ssr/spa-mode' 257 - fullPath: '/demo/start/ssr/spa-mode' 258 - preLoaderRoute: typeof DemoStartSsrSpaModeRouteImport 259 - parentRoute: typeof rootRouteImport 260 - } 261 - '/demo/start/ssr/full-ssr': { 262 - id: '/demo/start/ssr/full-ssr' 263 - path: '/demo/start/ssr/full-ssr' 264 - fullPath: '/demo/start/ssr/full-ssr' 265 - preLoaderRoute: typeof DemoStartSsrFullSsrRouteImport 266 - parentRoute: typeof rootRouteImport 267 - } 268 - '/demo/start/ssr/data-only': { 269 - id: '/demo/start/ssr/data-only' 270 - path: '/demo/start/ssr/data-only' 271 - fullPath: '/demo/start/ssr/data-only' 272 - preLoaderRoute: typeof DemoStartSsrDataOnlyRouteImport 273 - parentRoute: typeof rootRouteImport 274 - } 275 85 } 276 86 } 277 87 278 88 const rootRouteChildren: RootRouteChildren = { 279 89 IndexRoute: IndexRoute, 280 90 CardsIdRoute: CardsIdRoute, 281 - DemoTanstackQueryRoute: DemoTanstackQueryRoute, 282 91 CardsIndexRoute: CardsIndexRoute, 283 - DemoApiNamesRoute: DemoApiNamesRoute, 284 - DemoApiTqTodosRoute: DemoApiTqTodosRoute, 285 - DemoStartApiRequestRoute: DemoStartApiRequestRoute, 286 - DemoStartServerFuncsRoute: DemoStartServerFuncsRoute, 287 - DemoStartSsrDataOnlyRoute: DemoStartSsrDataOnlyRoute, 288 - DemoStartSsrFullSsrRoute: DemoStartSsrFullSsrRoute, 289 - DemoStartSsrSpaModeRoute: DemoStartSsrSpaModeRoute, 290 - DemoStartSsrIndexRoute: DemoStartSsrIndexRoute, 291 92 } 292 93 export const routeTree = rootRouteImport 293 94 ._addFileChildren(rootRouteChildren)
+23 -20
src/router.tsx
··· 1 - import { createRouter } from '@tanstack/react-router' 2 - import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query' 3 - import * as TanstackQuery from './integrations/tanstack-query/root-provider' 1 + import { createRouter } from "@tanstack/react-router"; 2 + import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query"; 3 + import * as TanstackQuery from "./integrations/tanstack-query/root-provider"; 4 4 5 5 // Import the generated route tree 6 - import { routeTree } from './routeTree.gen' 6 + import { routeTree } from "./routeTree.gen"; 7 7 8 8 // Create a new router instance 9 9 export const getRouter = () => { 10 - const rqContext = TanstackQuery.getContext() 10 + const rqContext = TanstackQuery.getContext(); 11 11 12 - const router = createRouter({ 13 - routeTree, 14 - context: { ...rqContext }, 15 - defaultPreload: 'intent', 16 - Wrap: (props: { children: React.ReactNode }) => { 17 - return ( 18 - <TanstackQuery.Provider {...rqContext}> 19 - {props.children} 20 - </TanstackQuery.Provider> 21 - ) 22 - }, 23 - }) 12 + const router = createRouter({ 13 + routeTree, 14 + context: { ...rqContext }, 15 + defaultPreload: "intent", 16 + Wrap: (props: { children: React.ReactNode }) => { 17 + return ( 18 + <TanstackQuery.Provider {...rqContext}> 19 + {props.children} 20 + </TanstackQuery.Provider> 21 + ); 22 + }, 23 + }); 24 24 25 - setupRouterSsrQueryIntegration({ router, queryClient: rqContext.queryClient }) 25 + setupRouterSsrQueryIntegration({ 26 + router, 27 + queryClient: rqContext.queryClient, 28 + }); 26 29 27 - return router 28 - } 30 + return router; 31 + };
+72 -61
src/routes/__root.tsx
··· 1 + import { TanStackDevtools } from "@tanstack/react-devtools"; 2 + import type { QueryClient } from "@tanstack/react-query"; 1 3 import { 2 - HeadContent, 3 - Scripts, 4 - createRootRouteWithContext, 5 - } from '@tanstack/react-router' 6 - import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' 7 - import { TanStackDevtools } from '@tanstack/react-devtools' 8 - 9 - import Header from '../components/Header' 10 - 11 - import TanStackQueryDevtools from '../integrations/tanstack-query/devtools' 12 - 13 - import appCss from '../styles.css?url' 14 - 15 - import type { QueryClient } from '@tanstack/react-query' 4 + createRootRouteWithContext, 5 + HeadContent, 6 + Scripts, 7 + } from "@tanstack/react-router"; 8 + import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; 9 + import Header from "../components/Header"; 10 + import TanStackQueryDevtools from "../integrations/tanstack-query/devtools"; 11 + import type { CardDataOutput } from "../lib/scryfall-types"; 12 + import appCss from "../styles.css?url"; 16 13 17 14 interface MyRouterContext { 18 - queryClient: QueryClient 15 + queryClient: QueryClient; 19 16 } 20 17 21 18 export const Route = createRootRouteWithContext<MyRouterContext>()({ 22 - head: () => ({ 23 - meta: [ 24 - { 25 - charSet: 'utf-8', 26 - }, 27 - { 28 - name: 'viewport', 29 - content: 'width=device-width, initial-scale=1', 30 - }, 31 - { 32 - title: 'TanStack Start Starter', 33 - }, 34 - ], 35 - links: [ 36 - { 37 - rel: 'stylesheet', 38 - href: appCss, 39 - }, 40 - ], 41 - }), 19 + loader: async ({ context }) => { 20 + await context.queryClient.ensureQueryData({ 21 + queryKey: ["cards"], 22 + queryFn: async () => { 23 + const response = await fetch("/data/cards.json"); 24 + if (!response.ok) { 25 + throw new Error("Failed to load card data"); 26 + } 27 + return response.json() as Promise<CardDataOutput>; 28 + }, 29 + staleTime: Number.POSITIVE_INFINITY, 30 + }); 31 + }, 32 + 33 + head: () => ({ 34 + meta: [ 35 + { 36 + charSet: "utf-8", 37 + }, 38 + { 39 + name: "viewport", 40 + content: "width=device-width, initial-scale=1", 41 + }, 42 + { 43 + title: "DeckBelcher", 44 + }, 45 + ], 46 + links: [ 47 + { 48 + rel: "stylesheet", 49 + href: appCss, 50 + }, 51 + ], 52 + }), 42 53 43 - shellComponent: RootDocument, 44 - }) 54 + shellComponent: RootDocument, 55 + }); 45 56 46 57 function RootDocument({ children }: { children: React.ReactNode }) { 47 - return ( 48 - <html lang="en"> 49 - <head> 50 - <HeadContent /> 51 - </head> 52 - <body> 53 - <Header /> 54 - {children} 55 - <TanStackDevtools 56 - config={{ 57 - position: 'bottom-right', 58 - }} 59 - plugins={[ 60 - { 61 - name: 'Tanstack Router', 62 - render: <TanStackRouterDevtoolsPanel />, 63 - }, 64 - TanStackQueryDevtools, 65 - ]} 66 - /> 67 - <Scripts /> 68 - </body> 69 - </html> 70 - ) 58 + return ( 59 + <html lang="en"> 60 + <head> 61 + <HeadContent /> 62 + </head> 63 + <body> 64 + <Header /> 65 + {children} 66 + <TanStackDevtools 67 + config={{ 68 + position: "bottom-right", 69 + }} 70 + plugins={[ 71 + { 72 + name: "Tanstack Router", 73 + render: <TanStackRouterDevtoolsPanel />, 74 + }, 75 + TanStackQueryDevtools, 76 + ]} 77 + /> 78 + <Scripts /> 79 + </body> 80 + </html> 81 + ); 71 82 }
+15 -40
src/routes/cards/$id.tsx
··· 1 1 import { useQuery } from "@tanstack/react-query"; 2 2 import { createFileRoute } from "@tanstack/react-router"; 3 3 import { ArrowLeft } from "lucide-react"; 4 + import { CardImage, CardPreview } from "../../components/CardImage"; 5 + import { ManaCost } from "../../components/ManaCost"; 4 6 import type { Card, CardDataOutput } from "../../lib/scryfall-types"; 5 7 import { isScryfallId } from "../../lib/scryfall-types"; 6 - import { getImageUri } from "../../lib/scryfall-utils"; 7 8 8 9 export const Route = createFileRoute("/cards/$id")({ 9 10 component: CardDetailPage, ··· 11 12 12 13 function CardDetailPage() { 13 14 const { id } = Route.useParams(); 14 - 15 - const { 16 - data: cardsData, 17 - isLoading, 18 - error, 19 - } = useQuery<CardDataOutput>({ 15 + const { data: cardsData } = useQuery<CardDataOutput>({ 20 16 queryKey: ["cards"], 21 - queryFn: async () => { 22 - const response = await fetch("/data/cards.json"); 23 - if (!response.ok) { 24 - throw new Error("Failed to load card data"); 25 - } 26 - return response.json(); 27 - }, 28 17 staleTime: Number.POSITIVE_INFINITY, 29 18 }); 30 19 31 - if (isLoading) { 32 - return ( 33 - <div className="min-h-screen bg-slate-900 flex items-center justify-center"> 34 - <p className="text-white text-lg">Loading card...</p> 35 - </div> 36 - ); 37 - } 38 - 39 - if (error || !cardsData) { 20 + if (!cardsData) { 40 21 return ( 41 22 <div className="min-h-screen bg-slate-900 flex items-center justify-center"> 42 23 <p className="text-red-400 text-lg">Failed to load card data</p> ··· 79 60 80 61 <div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> 81 62 <div className="flex justify-center lg:justify-end"> 82 - <img 83 - src={getImageUri(card.id, "large")} 84 - alt={card.name} 63 + <CardImage 64 + card={card} 65 + size="large" 85 66 className="rounded-xl shadow-2xl max-w-full h-auto" 86 67 /> 87 68 </div> ··· 92 73 {card.name} 93 74 </h1> 94 75 {card.mana_cost && ( 95 - <p className="text-xl text-gray-300 font-mono"> 96 - {card.mana_cost} 97 - </p> 76 + <div className="mb-2"> 77 + <ManaCost cost={card.mana_cost} size="large" /> 78 + </div> 98 79 )} 99 80 {card.type_line && ( 100 81 <p className="text-lg text-gray-400 mt-2">{card.type_line}</p> ··· 166 147 {otherPrintings.slice(0, 12).map((printId) => { 167 148 const printing = cardsData.cards[printId]; 168 149 return ( 169 - <a 150 + <CardPreview 170 151 key={printId} 152 + cardId={printId} 153 + name={printing.name} 154 + setName={printing.set_name} 171 155 href={`/cards/${printId}`} 172 - className="aspect-[5/7] rounded overflow-hidden hover:ring-2 hover:ring-cyan-500 transition-all" 173 - title={printing.set_name} 174 - > 175 - <img 176 - src={getImageUri(printId, "small")} 177 - alt={printing.name} 178 - className="w-full h-full object-cover" 179 - loading="lazy" 180 - /> 181 - </a> 156 + /> 182 157 ); 183 158 })} 184 159 </div>
+7 -42
src/routes/cards/index.tsx
··· 2 2 import { createFileRoute } from "@tanstack/react-router"; 3 3 import { Search } from "lucide-react"; 4 4 import { useState } from "react"; 5 + import { CardThumbnail } from "../../components/CardImage"; 5 6 import type { CardDataOutput } from "../../lib/scryfall-types"; 6 - import { getImageUri } from "../../lib/scryfall-utils"; 7 7 8 8 export const Route = createFileRoute("/cards/")({ 9 9 component: CardsPage, ··· 11 11 12 12 function CardsPage() { 13 13 const [searchQuery, setSearchQuery] = useState(""); 14 - 15 - const { data, isLoading, error } = useQuery<CardDataOutput>({ 14 + const { data } = useQuery<CardDataOutput>({ 16 15 queryKey: ["cards"], 17 - queryFn: async () => { 18 - const response = await fetch("/data/cards.json"); 19 - if (!response.ok) { 20 - throw new Error("Failed to load card data"); 21 - } 22 - return response.json(); 23 - }, 24 - staleTime: Number.POSITIVE_INFINITY, // Static data, never refetch 16 + staleTime: Number.POSITIVE_INFINITY, 25 17 }); 26 18 27 - if (isLoading) { 28 - return ( 29 - <div className="min-h-screen bg-slate-900 flex items-center justify-center"> 30 - <p className="text-white text-lg">Loading cards...</p> 31 - </div> 32 - ); 33 - } 34 - 35 - if (error || !data) { 19 + if (!data) { 36 20 return ( 37 21 <div className="min-h-screen bg-slate-900 flex items-center justify-center"> 38 22 <p className="text-red-400 text-lg">Failed to load card data</p> ··· 84 68 85 69 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4"> 86 70 {filteredCards.map((card) => ( 87 - <a 71 + <CardThumbnail 88 72 key={card.id} 73 + card={card} 89 74 href={`/cards/${card.id}`} 90 - className="group relative aspect-[5/7] rounded-lg overflow-hidden bg-slate-800 hover:ring-2 hover:ring-cyan-500 transition-all" 91 - > 92 - <img 93 - src={getImageUri(card.id, "small")} 94 - alt={card.name} 95 - className="w-full h-full object-cover" 96 - loading="lazy" 97 - /> 98 - <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/0 to-black/0 opacity-0 group-hover:opacity-100 transition-opacity"> 99 - <div className="absolute bottom-0 left-0 right-0 p-3"> 100 - <p className="text-white font-semibold text-sm line-clamp-2"> 101 - {card.name} 102 - </p> 103 - {card.set_name && ( 104 - <p className="text-gray-300 text-xs mt-1"> 105 - {card.set_name} 106 - </p> 107 - )} 108 - </div> 109 - </div> 110 - </a> 75 + /> 111 76 ))} 112 77 </div> 113 78 </div>
-10
src/routes/demo/api.names.ts
··· 1 - import { createFileRoute } from '@tanstack/react-router' 2 - import { json } from '@tanstack/react-start' 3 - 4 - export const Route = createFileRoute('/demo/api/names')({ 5 - server: { 6 - handlers: { 7 - GET: () => json(['Alice', 'Bob', 'Charlie']), 8 - }, 9 - }, 10 - })
-35
src/routes/demo/api.tq-todos.ts
··· 1 - import { createFileRoute } from '@tanstack/react-router' 2 - 3 - const todos = [ 4 - { 5 - id: 1, 6 - name: 'Buy groceries', 7 - }, 8 - { 9 - id: 2, 10 - name: 'Buy mobile phone', 11 - }, 12 - { 13 - id: 3, 14 - name: 'Buy laptop', 15 - }, 16 - ] 17 - 18 - export const Route = createFileRoute('/demo/api/tq-todos')({ 19 - server: { 20 - handlers: { 21 - GET: () => { 22 - return Response.json(todos) 23 - }, 24 - POST: async ({ request }) => { 25 - const name = await request.json() 26 - const todo = { 27 - id: todos.length + 1, 28 - name, 29 - } 30 - todos.push(todo) 31 - return Response.json(todo) 32 - }, 33 - }, 34 - }, 35 - })
-43
src/routes/demo/start.api-request.tsx
··· 1 - import { useQuery } from '@tanstack/react-query' 2 - 3 - import { createFileRoute } from '@tanstack/react-router' 4 - 5 - function getNames() { 6 - return fetch('/demo/api/names').then((res) => res.json() as Promise<string[]>) 7 - } 8 - 9 - export const Route = createFileRoute('/demo/start/api-request')({ 10 - component: Home, 11 - }) 12 - 13 - function Home() { 14 - const { data: names = [] } = useQuery({ 15 - queryKey: ['names'], 16 - queryFn: getNames, 17 - }) 18 - 19 - return ( 20 - <div 21 - className="flex items-center justify-center min-h-screen p-4 text-white" 22 - style={{ 23 - backgroundColor: '#000', 24 - backgroundImage: 25 - 'radial-gradient(ellipse 60% 60% at 0% 100%, #444 0%, #222 60%, #000 100%)', 26 - }} 27 - > 28 - <div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10"> 29 - <h1 className="text-2xl mb-4">Start API Request Demo - Names List</h1> 30 - <ul className="mb-4 space-y-2"> 31 - {names.map((name) => ( 32 - <li 33 - key={name} 34 - className="bg-white/10 border border-white/20 rounded-lg p-3 backdrop-blur-sm shadow-md" 35 - > 36 - <span className="text-lg text-white">{name}</span> 37 - </li> 38 - ))} 39 - </ul> 40 - </div> 41 - </div> 42 - ) 43 - }
-109
src/routes/demo/start.server-funcs.tsx
··· 1 - import fs from 'node:fs' 2 - import { useCallback, useState } from 'react' 3 - import { createFileRoute, useRouter } from '@tanstack/react-router' 4 - import { createServerFn } from '@tanstack/react-start' 5 - 6 - /* 7 - const loggingMiddleware = createMiddleware().server( 8 - async ({ next, request }) => { 9 - console.log("Request:", request.url); 10 - return next(); 11 - } 12 - ); 13 - const loggedServerFunction = createServerFn({ method: "GET" }).middleware([ 14 - loggingMiddleware, 15 - ]); 16 - */ 17 - 18 - const TODOS_FILE = 'todos.json' 19 - 20 - async function readTodos() { 21 - return JSON.parse( 22 - await fs.promises.readFile(TODOS_FILE, 'utf-8').catch(() => 23 - JSON.stringify( 24 - [ 25 - { id: 1, name: 'Get groceries' }, 26 - { id: 2, name: 'Buy a new phone' }, 27 - ], 28 - null, 29 - 2, 30 - ), 31 - ), 32 - ) 33 - } 34 - 35 - const getTodos = createServerFn({ 36 - method: 'GET', 37 - }).handler(async () => await readTodos()) 38 - 39 - const addTodo = createServerFn({ method: 'POST' }) 40 - .inputValidator((d: string) => d) 41 - .handler(async ({ data }) => { 42 - const todos = await readTodos() 43 - todos.push({ id: todos.length + 1, name: data }) 44 - await fs.promises.writeFile(TODOS_FILE, JSON.stringify(todos, null, 2)) 45 - return todos 46 - }) 47 - 48 - export const Route = createFileRoute('/demo/start/server-funcs')({ 49 - component: Home, 50 - loader: async () => await getTodos(), 51 - }) 52 - 53 - function Home() { 54 - const router = useRouter() 55 - let todos = Route.useLoaderData() 56 - 57 - const [todo, setTodo] = useState('') 58 - 59 - const submitTodo = useCallback(async () => { 60 - todos = await addTodo({ data: todo }) 61 - setTodo('') 62 - router.invalidate() 63 - }, [addTodo, todo]) 64 - 65 - return ( 66 - <div 67 - className="flex items-center justify-center min-h-screen bg-gradient-to-br from-zinc-800 to-black p-4 text-white" 68 - style={{ 69 - backgroundImage: 70 - 'radial-gradient(50% 50% at 20% 60%, #23272a 0%, #18181b 50%, #000000 100%)', 71 - }} 72 - > 73 - <div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10"> 74 - <h1 className="text-2xl mb-4">Start Server Functions - Todo Example</h1> 75 - <ul className="mb-4 space-y-2"> 76 - {todos?.map((t) => ( 77 - <li 78 - key={t.id} 79 - className="bg-white/10 border border-white/20 rounded-lg p-3 backdrop-blur-sm shadow-md" 80 - > 81 - <span className="text-lg text-white">{t.name}</span> 82 - </li> 83 - ))} 84 - </ul> 85 - <div className="flex flex-col gap-2"> 86 - <input 87 - type="text" 88 - value={todo} 89 - onChange={(e) => setTodo(e.target.value)} 90 - onKeyDown={(e) => { 91 - if (e.key === 'Enter') { 92 - submitTodo() 93 - } 94 - }} 95 - placeholder="Enter a new todo..." 96 - className="w-full px-4 py-3 rounded-lg border border-white/20 bg-white/10 backdrop-blur-sm text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent" 97 - /> 98 - <button 99 - disabled={todo.trim().length === 0} 100 - onClick={submitTodo} 101 - className="bg-blue-500 hover:bg-blue-600 disabled:bg-blue-500/50 disabled:cursor-not-allowed text-white font-bold py-3 px-4 rounded-lg transition-colors" 102 - > 103 - Add todo 104 - </button> 105 - </div> 106 - </div> 107 - </div> 108 - ) 109 - }
-41
src/routes/demo/start.ssr.data-only.tsx
··· 1 - import { createFileRoute } from '@tanstack/react-router' 2 - import { getPunkSongs } from '@/data/demo.punk-songs' 3 - 4 - export const Route = createFileRoute('/demo/start/ssr/data-only')({ 5 - ssr: 'data-only', 6 - component: RouteComponent, 7 - loader: async () => await getPunkSongs(), 8 - }) 9 - 10 - function RouteComponent() { 11 - const punkSongs = Route.useLoaderData() 12 - 13 - return ( 14 - <div 15 - className="flex items-center justify-center min-h-screen bg-gradient-to-br from-zinc-800 to-black p-4 text-white" 16 - style={{ 17 - backgroundImage: 18 - 'radial-gradient(50% 50% at 20% 60%, #1a1a1a 0%, #0a0a0a 50%, #000000 100%)', 19 - }} 20 - > 21 - <div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10"> 22 - <h1 className="text-3xl font-bold mb-6 text-pink-400"> 23 - Data Only SSR - Punk Songs 24 - </h1> 25 - <ul className="space-y-3"> 26 - {punkSongs.map((song) => ( 27 - <li 28 - key={song.id} 29 - className="bg-white/10 border border-white/20 rounded-lg p-4 backdrop-blur-sm shadow-md" 30 - > 31 - <span className="text-lg text-white font-medium"> 32 - {song.name} 33 - </span> 34 - <span className="text-white/60"> - {song.artist}</span> 35 - </li> 36 - ))} 37 - </ul> 38 - </div> 39 - </div> 40 - ) 41 - }
-40
src/routes/demo/start.ssr.full-ssr.tsx
··· 1 - import { createFileRoute } from '@tanstack/react-router' 2 - import { getPunkSongs } from '@/data/demo.punk-songs' 3 - 4 - export const Route = createFileRoute('/demo/start/ssr/full-ssr')({ 5 - component: RouteComponent, 6 - loader: async () => await getPunkSongs(), 7 - }) 8 - 9 - function RouteComponent() { 10 - const punkSongs = Route.useLoaderData() 11 - 12 - return ( 13 - <div 14 - className="flex items-center justify-center min-h-screen bg-gradient-to-br from-zinc-800 to-black p-4 text-white" 15 - style={{ 16 - backgroundImage: 17 - 'radial-gradient(50% 50% at 20% 60%, #1a1a1a 0%, #0a0a0a 50%, #000000 100%)', 18 - }} 19 - > 20 - <div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10"> 21 - <h1 className="text-3xl font-bold mb-6 text-purple-400"> 22 - Full SSR - Punk Songs 23 - </h1> 24 - <ul className="space-y-3"> 25 - {punkSongs.map((song) => ( 26 - <li 27 - key={song.id} 28 - className="bg-white/10 border border-white/20 rounded-lg p-4 backdrop-blur-sm shadow-md" 29 - > 30 - <span className="text-lg text-white font-medium"> 31 - {song.name} 32 - </span> 33 - <span className="text-white/60"> - {song.artist}</span> 34 - </li> 35 - ))} 36 - </ul> 37 - </div> 38 - </div> 39 - ) 40 - }
-43
src/routes/demo/start.ssr.index.tsx
··· 1 - import { createFileRoute, Link } from '@tanstack/react-router' 2 - 3 - export const Route = createFileRoute('/demo/start/ssr/')({ 4 - component: RouteComponent, 5 - }) 6 - 7 - function RouteComponent() { 8 - return ( 9 - <div 10 - className="flex items-center justify-center min-h-screen bg-gradient-to-br from-zinc-900 to-black p-4 text-white" 11 - style={{ 12 - backgroundImage: 13 - 'radial-gradient(50% 50% at 20% 60%, #1a1a1a 0%, #0a0a0a 50%, #000000 100%)', 14 - }} 15 - > 16 - <div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10"> 17 - <h1 className="text-4xl font-bold mb-8 text-center bg-gradient-to-r from-pink-500 via-purple-500 to-green-400 bg-clip-text text-transparent"> 18 - SSR Demos 19 - </h1> 20 - <div className="flex flex-col gap-4"> 21 - <Link 22 - to="/demo/start/ssr/spa-mode" 23 - className="text-2xl font-bold py-6 px-8 rounded-lg bg-gradient-to-r from-pink-600 to-pink-500 hover:from-pink-700 hover:to-pink-600 text-white text-center shadow-lg transform transition-all hover:scale-105 hover:shadow-pink-500/50 border-2 border-pink-400" 24 - > 25 - SPA Mode 26 - </Link> 27 - <Link 28 - to="/demo/start/ssr/full-ssr" 29 - className="text-2xl font-bold py-6 px-8 rounded-lg bg-gradient-to-r from-purple-600 to-purple-500 hover:from-purple-700 hover:to-purple-600 text-white text-center shadow-lg transform transition-all hover:scale-105 hover:shadow-purple-500/50 border-2 border-purple-400" 30 - > 31 - Full SSR 32 - </Link> 33 - <Link 34 - to="/demo/start/ssr/data-only" 35 - className="text-2xl font-bold py-6 px-8 rounded-lg bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white text-center shadow-lg transform transition-all hover:scale-105 hover:shadow-green-500/50 border-2 border-green-400" 36 - > 37 - Data Only 38 - </Link> 39 - </div> 40 - </div> 41 - </div> 42 - ) 43 - }
-47
src/routes/demo/start.ssr.spa-mode.tsx
··· 1 - import { useEffect, useState } from 'react' 2 - import { createFileRoute } from '@tanstack/react-router' 3 - import { getPunkSongs } from '@/data/demo.punk-songs' 4 - 5 - export const Route = createFileRoute('/demo/start/ssr/spa-mode')({ 6 - ssr: false, 7 - component: RouteComponent, 8 - }) 9 - 10 - function RouteComponent() { 11 - const [punkSongs, setPunkSongs] = useState< 12 - Awaited<ReturnType<typeof getPunkSongs>> 13 - >([]) 14 - 15 - useEffect(() => { 16 - getPunkSongs().then(setPunkSongs) 17 - }, []) 18 - 19 - return ( 20 - <div 21 - className="flex items-center justify-center min-h-screen bg-gradient-to-br from-zinc-800 to-black p-4 text-white" 22 - style={{ 23 - backgroundImage: 24 - 'radial-gradient(50% 50% at 20% 60%, #1a1a1a 0%, #0a0a0a 50%, #000000 100%)', 25 - }} 26 - > 27 - <div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10"> 28 - <h1 className="text-3xl font-bold mb-6 text-green-400"> 29 - SPA Mode - Punk Songs 30 - </h1> 31 - <ul className="space-y-3"> 32 - {punkSongs.map((song) => ( 33 - <li 34 - key={song.id} 35 - className="bg-white/10 border border-white/20 rounded-lg p-4 backdrop-blur-sm shadow-md" 36 - > 37 - <span className="text-lg text-white font-medium"> 38 - {song.name} 39 - </span> 40 - <span className="text-white/60"> - {song.artist}</span> 41 - </li> 42 - ))} 43 - </ul> 44 - </div> 45 - </div> 46 - ) 47 - }
-81
src/routes/demo/tanstack-query.tsx
··· 1 - import { useCallback, useState } from 'react' 2 - import { createFileRoute } from '@tanstack/react-router' 3 - import { useQuery, useMutation } from '@tanstack/react-query' 4 - 5 - export const Route = createFileRoute('/demo/tanstack-query')({ 6 - component: TanStackQueryDemo, 7 - }) 8 - 9 - type Todo = { 10 - id: number 11 - name: string 12 - } 13 - 14 - function TanStackQueryDemo() { 15 - const { data, refetch } = useQuery<Todo[]>({ 16 - queryKey: ['todos'], 17 - queryFn: () => fetch('/demo/api/tq-todos').then((res) => res.json()), 18 - initialData: [], 19 - }) 20 - 21 - const { mutate: addTodo } = useMutation({ 22 - mutationFn: (todo: string) => 23 - fetch('/demo/api/tq-todos', { 24 - method: 'POST', 25 - body: JSON.stringify(todo), 26 - }).then((res) => res.json()), 27 - onSuccess: () => refetch(), 28 - }) 29 - 30 - const [todo, setTodo] = useState('') 31 - 32 - const submitTodo = useCallback(async () => { 33 - await addTodo(todo) 34 - setTodo('') 35 - }, [addTodo, todo]) 36 - 37 - return ( 38 - <div 39 - className="flex items-center justify-center min-h-screen bg-gradient-to-br from-red-900 via-red-800 to-black p-4 text-white" 40 - style={{ 41 - backgroundImage: 42 - 'radial-gradient(50% 50% at 80% 20%, #3B021F 0%, #7B1028 60%, #1A000A 100%)', 43 - }} 44 - > 45 - <div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10"> 46 - <h1 className="text-2xl mb-4">TanStack Query Todos list</h1> 47 - <ul className="mb-4 space-y-2"> 48 - {data?.map((t) => ( 49 - <li 50 - key={t.id} 51 - className="bg-white/10 border border-white/20 rounded-lg p-3 backdrop-blur-sm shadow-md" 52 - > 53 - <span className="text-lg text-white">{t.name}</span> 54 - </li> 55 - ))} 56 - </ul> 57 - <div className="flex flex-col gap-2"> 58 - <input 59 - type="text" 60 - value={todo} 61 - onChange={(e) => setTodo(e.target.value)} 62 - onKeyDown={(e) => { 63 - if (e.key === 'Enter') { 64 - submitTodo() 65 - } 66 - }} 67 - placeholder="Enter a new todo..." 68 - className="w-full px-4 py-3 rounded-lg border border-white/20 bg-white/10 backdrop-blur-sm text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent" 69 - /> 70 - <button 71 - disabled={todo.trim().length === 0} 72 - onClick={submitTodo} 73 - className="bg-blue-500 hover:bg-blue-600 disabled:bg-blue-500/50 disabled:cursor-not-allowed text-white font-bold py-3 px-4 rounded-lg transition-colors" 74 - > 75 - Add todo 76 - </button> 77 - </div> 78 - </div> 79 - </div> 80 - ) 81 - }
+18 -114
src/routes/index.tsx
··· 1 - import { createFileRoute } from '@tanstack/react-router' 2 - import { 3 - Zap, 4 - Server, 5 - Route as RouteIcon, 6 - Shield, 7 - Waves, 8 - Sparkles, 9 - } from 'lucide-react' 1 + import { createFileRoute, Link } from "@tanstack/react-router"; 10 2 11 - export const Route = createFileRoute('/')({ component: App }) 3 + export const Route = createFileRoute("/")({ component: App }); 12 4 13 5 function App() { 14 - const features = [ 15 - { 16 - icon: <Zap className="w-12 h-12 text-cyan-400" />, 17 - title: 'Powerful Server Functions', 18 - description: 19 - 'Write server-side code that seamlessly integrates with your client components. Type-safe, secure, and simple.', 20 - }, 21 - { 22 - icon: <Server className="w-12 h-12 text-cyan-400" />, 23 - title: 'Flexible Server Side Rendering', 24 - description: 25 - 'Full-document SSR, streaming, and progressive enhancement out of the box. Control exactly what renders where.', 26 - }, 27 - { 28 - icon: <RouteIcon className="w-12 h-12 text-cyan-400" />, 29 - title: 'API Routes', 30 - description: 31 - 'Build type-safe API endpoints alongside your application. No separate backend needed.', 32 - }, 33 - { 34 - icon: <Shield className="w-12 h-12 text-cyan-400" />, 35 - title: 'Strongly Typed Everything', 36 - description: 37 - 'End-to-end type safety from server to client. Catch errors before they reach production.', 38 - }, 39 - { 40 - icon: <Waves className="w-12 h-12 text-cyan-400" />, 41 - title: 'Full Streaming Support', 42 - description: 43 - 'Stream data from server to client progressively. Perfect for AI applications and real-time updates.', 44 - }, 45 - { 46 - icon: <Sparkles className="w-12 h-12 text-cyan-400" />, 47 - title: 'Next Generation Ready', 48 - description: 49 - 'Built from the ground up for modern web applications. Deploy anywhere JavaScript runs.', 50 - }, 51 - ] 52 - 53 - return ( 54 - <div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900"> 55 - <section className="relative py-20 px-6 text-center overflow-hidden"> 56 - <div className="absolute inset-0 bg-gradient-to-r from-cyan-500/10 via-blue-500/10 to-purple-500/10"></div> 57 - <div className="relative max-w-5xl mx-auto"> 58 - <div className="flex items-center justify-center gap-6 mb-6"> 59 - <img 60 - src="/tanstack-circle-logo.png" 61 - alt="TanStack Logo" 62 - className="w-24 h-24 md:w-32 md:h-32" 63 - /> 64 - <h1 className="text-6xl md:text-7xl font-black text-white [letter-spacing:-0.08em]"> 65 - <span className="text-gray-300">TANSTACK</span>{' '} 66 - <span className="bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent"> 67 - START 68 - </span> 69 - </h1> 70 - </div> 71 - <p className="text-2xl md:text-3xl text-gray-300 mb-4 font-light"> 72 - The framework for next generation AI applications 73 - </p> 74 - <p className="text-lg text-gray-400 max-w-3xl mx-auto mb-8"> 75 - Full-stack framework powered by TanStack Router for React and Solid. 76 - Build modern applications with server functions, streaming, and type 77 - safety. 78 - </p> 79 - <div className="flex flex-col items-center gap-4"> 80 - <a 81 - href="https://tanstack.com/start" 82 - target="_blank" 83 - rel="noopener noreferrer" 84 - className="px-8 py-3 bg-cyan-500 hover:bg-cyan-600 text-white font-semibold rounded-lg transition-colors shadow-lg shadow-cyan-500/50" 85 - > 86 - Documentation 87 - </a> 88 - <p className="text-gray-400 text-sm mt-2"> 89 - Begin your TanStack Start journey by editing{' '} 90 - <code className="px-2 py-1 bg-slate-700 rounded text-cyan-400"> 91 - /src/routes/index.tsx 92 - </code> 93 - </p> 94 - </div> 95 - </div> 96 - </section> 97 - 98 - <section className="py-16 px-6 max-w-7xl mx-auto"> 99 - <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> 100 - {features.map((feature, index) => ( 101 - <div 102 - key={index} 103 - className="bg-slate-800/50 backdrop-blur-sm border border-slate-700 rounded-xl p-6 hover:border-cyan-500/50 transition-all duration-300 hover:shadow-lg hover:shadow-cyan-500/10" 104 - > 105 - <div className="mb-4">{feature.icon}</div> 106 - <h3 className="text-xl font-semibold text-white mb-3"> 107 - {feature.title} 108 - </h3> 109 - <p className="text-gray-400 leading-relaxed"> 110 - {feature.description} 111 - </p> 112 - </div> 113 - ))} 114 - </div> 115 - </section> 116 - </div> 117 - ) 6 + return ( 7 + <div className="min-h-screen bg-slate-900 flex items-center justify-center px-6"> 8 + <div className="text-center max-w-2xl"> 9 + <h1 className="text-6xl font-bold text-white mb-6">DeckBelcher</h1> 10 + <p className="text-xl text-gray-400 mb-8"> 11 + MTG deck building and sharing powered by AT Protocol 12 + </p> 13 + <Link 14 + to="/cards" 15 + className="inline-block px-8 py-3 bg-cyan-500 hover:bg-cyan-600 text-white font-semibold rounded-lg transition-colors" 16 + > 17 + Browse Cards 18 + </Link> 19 + </div> 20 + </div> 21 + ); 118 22 }