diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b8100b7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,111 @@ +--- +description: Use Bun instead of Node.js, npm, pnpm, or vite. +globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json" +alwaysApply: false +--- + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; + +// import .css files directly and it works +import './index.css'; + +import { createRoot } from "react-dom/client"; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. diff --git a/bun.lock b/bun.lock index 23f13eb..991e369 100644 --- a/bun.lock +++ b/bun.lock @@ -11,8 +11,8 @@ "@testing-library/jest-dom": "^6.1.4", "@testing-library/react": "^16.3.0", "@types/bun": "1.3.3", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", + "@types/react": "^19", + "@types/react-dom": "^19", "copyfiles": "^2.4.1", "eslint": "^7.19.0", "eslint-plugin-react-hooks": "^4.6.2", @@ -22,15 +22,30 @@ "preact": "^10.23.2", "preact-render-to-string": "^6.5.9", "prettier": "^2.4.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^19", + "react-dom": "^19", "size-limit": "^11.2.0", "typescript": "^5.8.0", }, }, + "packages/magazin": { + "name": "magazin", + "dependencies": { + "@dr.pogodin/react-helmet": "^3.0.4", + "bun-plugin-tailwind": "^0.1.2", + "tailwindcss": "^4.1.17", + "wouter": "workspace:*", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, "packages/wouter": { "name": "wouter", - "version": "3.7.1", + "version": "3.8.1", "dependencies": { "mitt": "^3.0.1", "regexparam": "^3.0.0", @@ -42,7 +57,7 @@ }, "packages/wouter-preact": { "name": "wouter-preact", - "version": "3.7.1", + "version": "3.8.1", "dependencies": { "mitt": "^3.0.1", "regexparam": "^3.0.0", @@ -66,6 +81,8 @@ "@babel/runtime": ["@babel/runtime@7.27.1", "", {}, "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="], + "@dr.pogodin/react-helmet": ["@dr.pogodin/react-helmet@3.0.4", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "react": "19" } }, "sha512-TesfNpzO12qcbyqKyWGDIYTdwVxD3pJv75rE/zhKUq/k9yxeP0BpOdHQ5cc1zA3j/GyY7CuIZjAUXmsxqI1/yw=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="], @@ -124,6 +141,28 @@ "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@1.2.1", "", {}, "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="], + "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w=="], + + "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-xGDePueVFrNgkS+iN0QdEFeRrx2MQ5hQ9ipRFu7N73rgoSSJsFlOKKt2uGZzunczedViIfjYl0ii0K4E9aZ0Ow=="], + + "@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ij4wQ9ECLFf1XFry+IFUN+28if40ozDqq6+QtuyOhIwraKzXOlAUbILhRMGvM3ED3yBex2mTwlKpA4Vja/V2g=="], + + "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-DabZ3Mt1XcJneWdEEug8l7bCPVvDBRBpjUIpNnRnMFWFnzr8KBEpMcaWTwYOghjXyJdhB4MPKb19MwqyQ+FHAw=="], + + "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-XWQ3tV/gtZj0wn2AdSUq/tEOKWT4OY+Uww70EbODgrrq00jxuTfq5nnYP6rkLD0M/T5BHJdQRSfQYdIni9vldw=="], + + "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-7eIARtKZKZDtah1aCpQUj/1/zT/zHRR063J6oAxZP9AuA547j5B9OM2D/vi/F4En7Gjk9FPjgPGTSYeqpQDzJw=="], + + "@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-IU8pxhIf845psOv55LqJyL+tSUc6HHMfs6FGhuJcAnyi92j+B1HjOhnFQh9MW4vjoo7do5F8AerXlvk59RGH2w=="], + + "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-xNSDRPn1yyObKteS8fyQogwsS4eCECswHHgaKM+/d4wy/omZQrXn8ZyGm/ZF9B73UfQytUfbhE7nEnrFq03f0w=="], + + "@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-JoRTPdAXRkNYouUlJqEncMWUKn/3DiWP03A7weBbtbsKr787gcdNna2YeyQKCb1lIXE4v1k18RM3gaOpQobGIQ=="], + + "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-kWqa1LKvDdAIzyfHxo3zGz3HFWbFHDlrNK77hKjUN42ycikvZJ+SHSX76+1OW4G8wmLETX4Jj+4BM1y01DQRIQ=="], + + "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-u5eZHKq6TPJSE282KyBOicGQ2trkFml0RoUfqkPOJVo7TXGrsGYYzdsugZRnVQY/WEmnxGtBy4T3PAaPqgQViA=="], + "@size-limit/esbuild": ["@size-limit/esbuild@11.2.0", "", { "dependencies": { "esbuild": "^0.25.0", "nanoid": "^5.1.0" }, "peerDependencies": { "size-limit": "11.2.0" } }, "sha512-vSg9H0WxGQPRzDnBzeDyD9XT0Zdq0L+AI3+77/JhxznbSCMJMMr8ndaWVQRhOsixl97N0oD4pRFw2+R1Lcvi6A=="], "@size-limit/file": ["@size-limit/file@11.2.0", "", { "peerDependencies": { "size-limit": "11.2.0" } }, "sha512-OZHE3putEkQ/fgzz3Tp/0hSmfVo3wyTpOJSRNm6AmcwX4Nm9YtTfbQQ/hZRwbBFR23S7x2Sd9EbqYzngKwbRoA=="], @@ -144,11 +183,9 @@ "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], - "@types/prop-types": ["@types/prop-types@15.7.14", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="], + "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], - "@types/react": ["@types/react@18.3.22", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q=="], - - "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="], + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], @@ -174,6 +211,10 @@ "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + "bun": ["bun@1.3.3", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.3", "@oven/bun-darwin-x64": "1.3.3", "@oven/bun-darwin-x64-baseline": "1.3.3", "@oven/bun-linux-aarch64": "1.3.3", "@oven/bun-linux-aarch64-musl": "1.3.3", "@oven/bun-linux-x64": "1.3.3", "@oven/bun-linux-x64-baseline": "1.3.3", "@oven/bun-linux-x64-musl": "1.3.3", "@oven/bun-linux-x64-musl-baseline": "1.3.3", "@oven/bun-windows-x64": "1.3.3", "@oven/bun-windows-x64-baseline": "1.3.3" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-2hJ4ocTZ634/Ptph4lysvO+LbbRZq8fzRvMwX0/CqaLBxrF2UB5D1LdMB8qGcdtCer4/VR9Bx5ORub0yn+yzmw=="], + + "bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="], + "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], "bytes-iec": ["bytes-iec@3.1.1", "", {}, "sha512-fey6+4jDK7TFtFg/klGSvNKJctyU7n2aQdnM+CO0ruLPbqqMOM8Tio0Pc+deqUeVKX1tL5DQep1zQ7+37aTAsA=="], @@ -206,7 +247,7 @@ "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], @@ -344,10 +385,10 @@ "lodash.truncate": ["lodash.truncate@4.4.2", "", {}, "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw=="], - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + "magazin": ["magazin@workspace:packages/magazin"], + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -414,9 +455,9 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], - "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], @@ -440,7 +481,7 @@ "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -474,6 +515,8 @@ "table": ["table@6.9.0", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A=="], + "tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="], + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], "through2": ["through2@2.0.5", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="], @@ -528,6 +571,8 @@ "@babel/highlight/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + "@dr.pogodin/react-helmet/@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + "@testing-library/jest-dom/aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], "@testing-library/jest-dom/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], @@ -546,6 +591,8 @@ "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "magazin/@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "table/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -558,6 +605,8 @@ "@babel/highlight/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + "magazin/@types/bun/bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], + "table/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "through2/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], @@ -568,6 +617,8 @@ "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + "magazin/@types/bun/bun-types/@types/node": ["@types/node@22.15.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ=="], + "@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], } } diff --git a/package.json b/package.json index bb2fda0..c35342e 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,7 @@ "description": "A minimalistic routing for React and Preact. Monorepo package.", "type": "module", "workspaces": [ - "packages/wouter", - "packages/wouter-preact" + "packages/*" ], "scripts": { "fix:p": "prettier --write \"./**/*.(js|ts){x,}\"", @@ -139,8 +138,8 @@ "@testing-library/jest-dom": "^6.1.4", "@testing-library/react": "^16.3.0", "@types/bun": "1.3.3", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", + "@types/react": "^19", + "@types/react-dom": "^19", "copyfiles": "^2.4.1", "eslint": "^7.19.0", "eslint-plugin-react-hooks": "^4.6.2", @@ -150,9 +149,9 @@ "preact": "^10.23.2", "preact-render-to-string": "^6.5.9", "prettier": "^2.4.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^19", + "react-dom": "^19", "size-limit": "^11.2.0", "typescript": "^5.8.0" } -} \ No newline at end of file +} diff --git a/packages/magazin/.gitignore b/packages/magazin/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/packages/magazin/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/packages/magazin/App.tsx b/packages/magazin/App.tsx new file mode 100644 index 0000000..5a11b41 --- /dev/null +++ b/packages/magazin/App.tsx @@ -0,0 +1,49 @@ +import { Route, Switch, Redirect } from "wouter"; +import { Helmet } from "@dr.pogodin/react-helmet"; +import { HomePage } from "@/routes/home.tsx"; +import { AboutPage } from "@/routes/about.tsx"; +import { NotFoundPage } from "@/routes/404.tsx"; +import { ProductPage } from "@/routes/products/[slug].tsx"; +import { CartPage } from "@/routes/cart.tsx"; +import { WithStatusCode } from "@/components/with-status-code.tsx"; +import { Navbar } from "@/components/navbar.tsx"; + +export function App() { + return ( +
+ + + + +
+ + + + + + + + + + + {(params) => } + + + + + + + + + + + + + + + + +
+
+ ); +} diff --git a/packages/magazin/bun.lock b/packages/magazin/bun.lock new file mode 100644 index 0000000..d419404 --- /dev/null +++ b/packages/magazin/bun.lock @@ -0,0 +1,45 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "sourdough", + "dependencies": { + "react": "19", + "react-dom": "19", + "wouter": "3.8.0", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], + + "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + + "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], + + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], + + "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], + + "regexparam": ["regexparam@3.0.0", "", {}, "sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "wouter": ["wouter@3.8.0", "", { "dependencies": { "mitt": "^3.0.1", "regexparam": "^3.0.0", "use-sync-external-store": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-oFQKbZVQxaXGXSuUg++FETTORSKQUur1Q+tvIlVm9TjPt+bpzS/IkRy2mXdSuqTjJ/PorSPxbRz4UwWsICes1Q=="], + } +} diff --git a/packages/magazin/client.tsx b/packages/magazin/client.tsx new file mode 100644 index 0000000..7984e13 --- /dev/null +++ b/packages/magazin/client.tsx @@ -0,0 +1,13 @@ +import { hydrateRoot } from "react-dom/client"; +import { Router } from "wouter"; +import { HelmetProvider } from "@dr.pogodin/react-helmet"; +import { App } from "./App"; + +hydrateRoot( + document.body, + + + + + +); diff --git a/packages/magazin/components/navbar.tsx b/packages/magazin/components/navbar.tsx new file mode 100644 index 0000000..3a04559 --- /dev/null +++ b/packages/magazin/components/navbar.tsx @@ -0,0 +1,56 @@ +import { Link } from "wouter"; + +function Logo() { + return ; +} + +function NavLink({ + href, + children, +}: { + href: string; + children: React.ReactNode; +}) { + return ( + + `text-sm font-medium ${ + active ? "text-gray-900" : "text-gray-500 hover:text-gray-900" + }` + } + > + {children} + + ); +} + +export function Navbar() { + return ( + + ); +} diff --git a/packages/magazin/components/with-status-code.tsx b/packages/magazin/components/with-status-code.tsx new file mode 100644 index 0000000..51c5048 --- /dev/null +++ b/packages/magazin/components/with-status-code.tsx @@ -0,0 +1,19 @@ +import { useRouter } from "wouter"; + +export function WithStatusCode({ + code, + children, +}: { + code: number; + children: React.ReactNode; +}) { + const router = useRouter(); + + // Set status code on SSR context if available + // Cast to any because statusCode is not yet in the official types + if (router.ssrContext) { + (router.ssrContext as any).statusCode = code; + } + + return <>{children}; +} diff --git a/packages/magazin/db/products.ts b/packages/magazin/db/products.ts new file mode 100644 index 0000000..b98cfd7 --- /dev/null +++ b/packages/magazin/db/products.ts @@ -0,0 +1,106 @@ +export interface Product { + slug: string; + name: string; + price: number; + brand: string; + category: string; + image: string; + description: string; +} + +export const products: Product[] = [ + { + image: "/products/carabiner.webp", + slug: "hook-keyring-rvst", + brand: "RVST", + category: "Accessories", + name: "Hook Keyring", + price: 65, + description: + "Premium carabiner keyring crafted with attention to detail and designed for everyday carry.", + }, + { + image: "/products/ring.webp", + slug: "silver-ok-ring", + brand: "Rick Woens", + category: "Jewelry", + name: "Silver OK Ring", + price: 99, + description: + "Handcrafted sterling silver ring with a unique OK gesture design.", + }, + { + image: "/products/navigator-cap.webp", + slug: "navigator-baseball-cap", + brand: "Rendr", + category: "Accessories", + name: "Navigator Baseball Cap", + price: 179, + description: + "Premium baseball cap with embroidered branding and adjustable fit.", + }, + { + image: "/products/sizelimited-tshirt.webp", + slug: "size-limited-tshirt", + brand: "Rendr", + category: "Clothing", + name: "Size Limited T-Shirt", + price: 65, + description: + "Comfortable cotton t-shirt with minimalist branding and premium fit.", + }, + { + image: "/products/wouter-glasses.webp", + slug: "wouter-cult-glasses", + brand: "Wouter", + category: "Accessories", + name: "Wouter Glasses", + price: 129, + description: + "Cult glasses worn by wouter. Minimalist design with premium frames and crystal-clear lenses.", + }, + { + image: "/products/parka.webp", + slug: "route-breaker-windbreaker", + brand: "Wouter", + category: "Clothing", + name: "Route Breaker Windbreaker", + price: 249, + description: + "Navigate any weather with the Route Breaker. Lightweight, water-resistant, and built for those who hook into adventure.", + }, + { + image: "/products/react-pendant.webp", + slug: "react-state-pendant", + brand: "Wouter", + category: "Jewelry", + name: "React State Pendant", + price: 159, + description: + "A declarative pendant for those who embrace the component lifecycle. Hooks perfectly with any chain.", + }, + { + image: "/products/scarf.webp", + slug: "nested-routes-silk-scarf", + brand: "Wouter", + category: "Accessories", + name: "Nested Routes Silk Scarf", + price: 189, + description: + "Luxurious silk scarf featuring an intricate wouter pattern. Each layer wraps seamlessly into the next, just like your favorite routes.", + }, + { + image: "/products/poster-a.webp", + slug: "keep-routing-poster", + brand: "Wouter", + category: "Art", + name: "Keep Routing Poster", + price: 45, + description: + "Minimalist poster with a bold message for developers. Museum-quality print that reminds you to stay on the path.", + }, +]; + +export function getProductBySlug(slug: string): Product | undefined { + return products.find((p) => p.slug === slug); +} diff --git a/packages/magazin/index.html b/packages/magazin/index.html new file mode 100644 index 0000000..7939cf8 --- /dev/null +++ b/packages/magazin/index.html @@ -0,0 +1,16 @@ + + + + + + + + + <!-- injected during SSR --> + + + + diff --git a/packages/magazin/index.tsx b/packages/magazin/index.tsx new file mode 100644 index 0000000..4d12ee4 --- /dev/null +++ b/packages/magazin/index.tsx @@ -0,0 +1,136 @@ +import { renderToReadableStream } from "react-dom/server"; +import { Router } from "wouter"; +import { + HelmetProvider, + type HelmetDataContext, +} from "@dr.pogodin/react-helmet"; +import { App } from "./App.tsx"; +import tailwind from "bun-plugin-tailwind"; + +// Build the HTML and all its assets before starting the server +const build = await Bun.build({ + entrypoints: ["./index.html"], + // No outdir = files are kept in memory, not written to disk + minify: false, + publicPath: "/", + plugins: [tailwind], +}); + +if (!build.success) { + console.error("Build failed:", build.logs); + process.exit(1); +} + +// Create a map of assets by their path for quick lookup +const assets = new Map(); +let htmlTemplate: string | null = null; + +for (const output of build.outputs) { + // The HTML file will be used as template for SSR + if (output.path.endsWith(".html")) { + htmlTemplate = await output.text(); + } else { + // Store other assets (JS, CSS, etc.) by their basename + const basename = "/" + output.path.split("/").pop()!; + assets.set(basename, output); + } +} + +if (!htmlTemplate) { + console.error("No HTML template found in build outputs"); + process.exit(1); +} + +const port = process.env.PORT ? parseInt(process.env.PORT) : 3002; + +Bun.serve({ + port, + async fetch(req) { + const url = new URL(req.url); + + // Check if this is a request for a built asset + const asset = assets.get(url.pathname); + if (asset) { + return new Response(asset); + } + + // Check if this is a request for a static file from public/ + const publicFile = Bun.file(`./public${url.pathname}`); + if (await publicFile.exists()) { + return new Response(publicFile); + } + + // Otherwise, it's a page request - render with SSR + // ssrPath accepts full path with search, e.g. "/foo?bar=1" + // ssrContext is used to handle redirects and status codes on the server + const ssrContext: { redirectTo?: string; statusCode?: number } = {}; + const helmetContext: HelmetDataContext = {}; + + const stream = await renderToReadableStream( + + + + + + ); + + // Check if a redirect occurred during SSR + if (ssrContext.redirectTo) { + return Response.redirect( + new URL(ssrContext.redirectTo, url.origin).toString(), + 302 + ); + } + + // Get status code from context, default to 200 + const statusCode = ssrContext.statusCode || 200; + + // Convert stream to string + const appHtml = await new Response(stream).text(); + + const helmet = helmetContext.helmet; + + // Use HTMLRewriter to inject the SSR content into body and title + const rewriter = new HTMLRewriter() + .on("body", { + element(element) { + element.setInnerContent(appHtml, { html: true }); + }, + }) + .on("title", { + element(element) { + if (!helmet) return; + // Remove the existing title tag and let helmet's title be appended to head + element.remove(); + }, + }) + .on("head", { + element(element) { + if (!helmet) return; + + const headContent = [ + helmet.title?.toString(), + helmet.priority?.toString(), + helmet.meta?.toString(), + helmet.link?.toString(), + helmet.script?.toString(), + ] + .filter(Boolean) + .join("\n"); + + if (headContent) { + element.append(headContent, { html: true }); + } + }, + }); + + const transformedResponse = rewriter.transform(new Response(htmlTemplate)); + + return new Response(transformedResponse.body, { + status: statusCode, + headers: { "Content-Type": "text/html" }, + }); + }, +}); + +console.log(`Server running at http://localhost:${port}`); diff --git a/packages/magazin/package.json b/packages/magazin/package.json new file mode 100644 index 0000000..cf53231 --- /dev/null +++ b/packages/magazin/package.json @@ -0,0 +1,22 @@ +{ + "name": "magazin", + "module": "index.ts", + "type": "module", + "private": true, + "scripts": { + "dev": "bun --watch index.tsx", + "start": "bun index.tsx" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@dr.pogodin/react-helmet": "^3.0.4", + "bun-plugin-tailwind": "^0.1.2", + "tailwindcss": "^4.1.17", + "wouter": "workspace:*" + } +} diff --git a/packages/magazin/public/favicon.webp b/packages/magazin/public/favicon.webp new file mode 100644 index 0000000..af3e034 Binary files /dev/null and b/packages/magazin/public/favicon.webp differ diff --git a/packages/magazin/public/products/carabiner.webp b/packages/magazin/public/products/carabiner.webp new file mode 100644 index 0000000..8a1eb3a Binary files /dev/null and b/packages/magazin/public/products/carabiner.webp differ diff --git a/packages/magazin/public/products/navigator-cap.webp b/packages/magazin/public/products/navigator-cap.webp new file mode 100644 index 0000000..58882ca Binary files /dev/null and b/packages/magazin/public/products/navigator-cap.webp differ diff --git a/packages/magazin/public/products/parka.webp b/packages/magazin/public/products/parka.webp new file mode 100644 index 0000000..d5b1158 Binary files /dev/null and b/packages/magazin/public/products/parka.webp differ diff --git a/packages/magazin/public/products/poster-a.webp b/packages/magazin/public/products/poster-a.webp new file mode 100644 index 0000000..1d6479e Binary files /dev/null and b/packages/magazin/public/products/poster-a.webp differ diff --git a/packages/magazin/public/products/react-pendant.webp b/packages/magazin/public/products/react-pendant.webp new file mode 100644 index 0000000..f1bd94e Binary files /dev/null and b/packages/magazin/public/products/react-pendant.webp differ diff --git a/packages/magazin/public/products/ring.webp b/packages/magazin/public/products/ring.webp new file mode 100644 index 0000000..afba4d0 Binary files /dev/null and b/packages/magazin/public/products/ring.webp differ diff --git a/packages/magazin/public/products/scarf.webp b/packages/magazin/public/products/scarf.webp new file mode 100644 index 0000000..cf48f5f Binary files /dev/null and b/packages/magazin/public/products/scarf.webp differ diff --git a/packages/magazin/public/products/sizelimited-tshirt.webp b/packages/magazin/public/products/sizelimited-tshirt.webp new file mode 100644 index 0000000..d5d3b5a Binary files /dev/null and b/packages/magazin/public/products/sizelimited-tshirt.webp differ diff --git a/packages/magazin/public/products/wouter-glasses.webp b/packages/magazin/public/products/wouter-glasses.webp new file mode 100644 index 0000000..7b4686d Binary files /dev/null and b/packages/magazin/public/products/wouter-glasses.webp differ diff --git a/packages/magazin/routes/404.tsx b/packages/magazin/routes/404.tsx new file mode 100644 index 0000000..0708126 --- /dev/null +++ b/packages/magazin/routes/404.tsx @@ -0,0 +1,27 @@ +import { Link } from "wouter"; +import { Helmet } from "@dr.pogodin/react-helmet"; + +export function NotFoundPage() { + return ( +
+ + Page Not Found + +
+ 404 +
+ +

+ Not Found +

+

+ We are sorry, but the page you're looking for doesn't exist. Try going + back to the{" "} + + home page + + . +

+
+ ); +} diff --git a/packages/magazin/routes/about.tsx b/packages/magazin/routes/about.tsx new file mode 100644 index 0000000..f360b71 --- /dev/null +++ b/packages/magazin/routes/about.tsx @@ -0,0 +1,74 @@ +import { Link } from "wouter"; +import { Helmet } from "@dr.pogodin/react-helmet"; + +function Feature({ children }: { children: React.ReactNode }) { + return ( +
  • + + {children} +
  • + ); +} + +export function AboutPage() { + return ( + <> + + About + + +

    + What is this? +

    +

    + This is a simple SSR demo showcasing wouter v3.8.0, React 19 with + server-side rendering and client-side hydration running on Bun. +

    + +
    Features
    +
      + + + Dynamic segments + + + + + Default switch route (404) + + + + + Search parameters + + + + + Redirect with SSR support + + + + + Active links + + + + + Navigation with state + + + + + Custom status codes (404) + + + + View transitions (soon) + +
    + + ); +} diff --git a/packages/magazin/routes/cart.tsx b/packages/magazin/routes/cart.tsx new file mode 100644 index 0000000..9bd655b --- /dev/null +++ b/packages/magazin/routes/cart.tsx @@ -0,0 +1,105 @@ +import { useEffect, useState } from "react"; +import { useLocation } from "wouter"; +import { Helmet } from "@dr.pogodin/react-helmet"; +import { products, type Product } from "@/db/products"; + +const cartItems: Array<{ product: Product; quantity: number }> = [ + { product: products[4]!, quantity: 1 }, // Wouter Glasses + { product: products[5]!, quantity: 1 }, // Route Breaker Windbreaker + { product: products[0]!, quantity: 2 }, // Hook Keyring + { product: products[7]!, quantity: 3 }, // Keep Routing Poster +]; + +function NotificationBanner({ + show, + message, +}: { + show: boolean; + message: string | null; +}) { + if (!message) return null; + + return ( +
    +
    + + {message} added to cart +
    +
    + ); +} + +export function CartPage() { + const [location, navigate] = useLocation(); + const [showNotification, setShowNotification] = useState(false); + const [addedItem, setAddedItem] = useState(null); + + useEffect(() => { + const state = history.state as { addedItem?: string } | null; + if (state?.addedItem) { + setAddedItem(state.addedItem); + setShowNotification(true); + + // Clear the state so it doesn't show again on refresh + navigate(location, { replace: true, state: null }); + + // Hide notification after 3 seconds + const timer = setTimeout(() => { + setShowNotification(false); + }, 3000); + + return () => clearTimeout(timer); + } + }, [location, navigate]); + + return ( + <> + + Cart + + +

    + Shopping Cart +

    + +
    + {cartItems.map((item, index) => ( +
    +
    +
    + {item.product.name} +
    +
    + {item.product.name} + + {item.quantity} × ${item.product.price} + +
    +
    + + ${item.product.price} + +
    + ))} +
    + +
    +
    Total
    +
    $643
    +
    + + + + ); +} diff --git a/packages/magazin/routes/home.tsx b/packages/magazin/routes/home.tsx new file mode 100644 index 0000000..9a9f286 --- /dev/null +++ b/packages/magazin/routes/home.tsx @@ -0,0 +1,190 @@ +import { useSearchParams, Link } from "wouter"; +import { Helmet } from "@dr.pogodin/react-helmet"; +import { products, type Product } from "@/db/products"; + +function ProductCard({ slug, brand, category, name, price, image }: Product) { + return ( + +
    + {name} +
    +
    +
    + {brand} · {category} +
    +
    + {name} + ${price.toLocaleString()} +
    +
    + + ); +} + +const categories = [ + { value: "all", label: "All" }, + { value: "accessories", label: "Accessories" }, + { value: "clothing", label: "Clothing" }, + { value: "jewelry", label: "Jewelry" }, + { value: "art", label: "Art" }, +]; + +const sortOptions = [ + { value: "newest", label: "Newest" }, + { value: "price-asc", label: "Price: Low to High" }, + { value: "price-desc", label: "Price: High to Low" }, + { value: "name", label: "Name" }, +]; + +function CategoryFilter({ + value, + onChange, +}: { + value: string; + onChange: (value: string) => void; +}) { + return ( +
    + {categories.map((cat) => ( + + ))} +
    + ); +} + +function SortSelect({ + value, + onChange, +}: { + value: string; + onChange: (value: string) => void; +}) { + return ( +
    + + +
    + ); +} + +export function HomePage() { + const [searchParams, setSearchParams] = useSearchParams(); + const category = searchParams.get("category") || "all"; + const sort = searchParams.get("sort") || "newest"; + + const handleFilterChange = (key: string, value: string) => { + setSearchParams((params) => { + const newParams = new URLSearchParams(params); + if (value === "all" || value === "newest") { + newParams.delete(key); + } else { + newParams.set(key, value); + } + return newParams; + }); + }; + + // Filter products by category + let filteredProducts = products; + if (category !== "all") { + filteredProducts = products.filter( + (p) => p.category.toLowerCase() === category.toLowerCase() + ); + } + + // Sort products + const sortedProducts = [...filteredProducts].sort((a, b) => { + switch (sort) { + case "price-asc": + return a.price - b.price; + case "price-desc": + return b.price - a.price; + case "name": + return a.name.localeCompare(b.name); + case "newest": + default: + return 0; // Keep original order + } + }); + + return ( + <> + + Magazin by wouter + + +
    +

    + Welcome to our shop +

    +

    + Exclusive merch for hardcore wouter fans. You can't buy these yet, so + go star the repo to increase our chances of becoming a billion dollar + company. +

    + +
    + +
    + handleFilterChange("category", v)} + /> + handleFilterChange("sort", v)} + /> +
    + +
    + {sortedProducts.map((product) => ( + + ))} +
    + + {sortedProducts.length === 0 && ( +
    + No products found in this category. +
    + )} + + ); +} diff --git a/packages/magazin/routes/products/[slug].tsx b/packages/magazin/routes/products/[slug].tsx new file mode 100644 index 0000000..c7d9bcc --- /dev/null +++ b/packages/magazin/routes/products/[slug].tsx @@ -0,0 +1,68 @@ +import { Link } from "wouter"; +import { Helmet } from "@dr.pogodin/react-helmet"; +import { getProductBySlug } from "@/db/products"; + +export function ProductPage({ slug }: { slug: string }) { + const product = getProductBySlug(slug); + + if (!product) { + return ( +
    + + Product Not Found + +

    + Product not found +

    + + Back to home + +
    + ); + } + + return ( + <> + + {product.name} + + + + +
    +
    + {product.name} +
    +
    +
    + {product.brand} · {product.category} +
    +

    + {product.name} +

    +

    {product.description}

    + +
    + ${product.price} +
    +
    + + Add to Cart + +
    +
    +
    + + ); +} diff --git a/packages/magazin/styles.css b/packages/magazin/styles.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/packages/magazin/styles.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/packages/magazin/tsconfig.json b/packages/magazin/tsconfig.json new file mode 100644 index 0000000..ad87dbe --- /dev/null +++ b/packages/magazin/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Path aliases + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + }, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/packages/wouter/test/link.test-d.tsx b/packages/wouter/test/link.test-d.tsx index dd6cd7c..b9b31d0 100644 --- a/packages/wouter/test/link.test-d.tsx +++ b/packages/wouter/test/link.test-d.tsx @@ -93,15 +93,6 @@ describe(" with ref", () => { ; }); - test("should have error when type is `unknown`", () => { - const ref = React.useRef(); - - // @ts-expect-error - - Hello - ; - }); - test("should have error when type is miss matched", () => { const ref = React.useRef(null); diff --git a/packages/wouter/test/switch.test.tsx b/packages/wouter/test/switch.test.tsx index 2a6905a..cf9315f 100644 --- a/packages/wouter/test/switch.test.tsx +++ b/packages/wouter/test/switch.test.tsx @@ -4,7 +4,7 @@ import { Router, Route, Switch } from "../src/index.js"; import { memoryLocation } from "../src/memory-location.js"; import { render, act, cleanup } from "@testing-library/react"; -import { PropsWithChildren, ReactElement } from "react"; +import { PropsWithChildren, ReactElement, JSX } from "react"; // Clean up after each test to avoid DOM pollution afterEach(cleanup);