diff --git a/bun.lock b/bun.lock index 0b42284..63c0b77 100644 --- a/bun.lock +++ b/bun.lock @@ -49,6 +49,7 @@ "@trpc/react-query": "^11.12.0", "@trpc/server": "^11.12.0", "@uiw/react-md-editor": "^4.0.11", + "@uploadthing/react": "^7.3.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -80,6 +81,7 @@ "tailwind-merge": "^3.5.0", "tailwindcss-motion": "^1.1.1", "type-fest": "^5.4.4", + "uploadthing": "^7.7.4", "vaul": "^1.1.2", "zod": "^4.3.6", }, @@ -272,6 +274,8 @@ "@ecies/ciphers": ["@ecies/ciphers@0.2.5", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A=="], + "@effect/platform": ["@effect/platform@0.90.3", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.33.0", "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.17.7" } }, "sha512-XvQ37yzWQKih4Du2CYladd1i/MzqtgkTPNCaN6Ku6No4CK83hDtXIV/rP03nEoBg2R3Pqgz6gGWmE2id2G81HA=="], + "@electric-sql/pglite": ["@electric-sql/pglite@0.3.16", "", {}, "sha512-mZkZfOd9OqTMHsK+1cje8OSzfAQcpD7JmILXTl5ahdempjUDdmg4euf1biDex5/LfQIDJ3gvCu6qDgdnDxfJmA=="], "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], @@ -478,6 +482,18 @@ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="], + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], + + "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="], + + "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], + + "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], + "@mswjs/interceptors": ["@mswjs/interceptors@0.41.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], @@ -520,6 +536,8 @@ "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], @@ -708,7 +726,7 @@ "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@standard-schema/spec": ["@standard-schema/spec@1.0.0-beta.4", "", {}, "sha512-d3IxtzLo7P1oZ8s8YNvxzBUXRXojSut8pbPrTYtzsc5sn4+53jVqbk66pQerSZbZSJZQux6LkclB/+8IDordHg=="], "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], @@ -938,6 +956,12 @@ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], + "@uploadthing/mime-types": ["@uploadthing/mime-types@0.3.6", "", {}, "sha512-t3tTzgwFV9+1D7lNDYc7Lr7kBwotHaX0ZsvoCGe7xGnXKo9z0jG2Sjl/msll12FeoLj77nyhsxevXyGpQDBvLg=="], + + "@uploadthing/react": ["@uploadthing/react@7.3.3", "", { "dependencies": { "@uploadthing/shared": "7.1.10", "file-selector": "0.6.0" }, "peerDependencies": { "next": "*", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "uploadthing": "^7.2.0" }, "optionalPeers": ["next"] }, "sha512-GhKbK42jL2Qs7OhRd2Z6j0zTLsnJTRJH31nR7RZnUYVoRh2aS/NabMAnHBNqfunIAGXVaA717Pvzq7vtxuPTmQ=="], + + "@uploadthing/shared": ["@uploadthing/shared@7.1.10", "", { "dependencies": { "@uploadthing/mime-types": "0.3.6", "effect": "3.17.7", "sqids": "^0.3.0" } }, "sha512-R/XSA3SfCVnLIzFpXyGaKPfbwlYlWYSTuGjTFHuJhdAomuBuhopAHLh2Ois5fJibAHzi02uP1QCKbgTAdmArqg=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="], "@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.18", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.18", "ast-v8-to-istanbul": "^0.3.10", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.18", "vitest": "4.0.18" }, "optionalPeers": ["@vitest/browser"] }, "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg=="], @@ -1214,6 +1238,8 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "effect": ["effect@3.17.7", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-dpt0ONUn3zzAuul6k4nC/coTTw27AL5nhkORXgTi6NfMPzqWYa1M05oKmOMTxpVSTKepqXVcW9vIwkuaaqx9zA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], "embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="], @@ -1282,6 +1308,8 @@ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="], @@ -1304,10 +1332,14 @@ "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + "file-selector": ["file-selector@0.6.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], @@ -1786,8 +1818,14 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "msgpackr": ["msgpackr@1.11.9", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw=="], + + "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], + "msw": ["msw@2.12.10", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw=="], + "multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], + "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -1810,6 +1848,8 @@ "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], @@ -1934,7 +1974,7 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "pure-rand": ["pure-rand@7.0.1", "", {}, "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ=="], + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], @@ -2100,6 +2140,8 @@ "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "sqids": ["sqids@0.3.0", "", {}, "sha512-lOQK1ucVg+W6n3FhRwwSeUijxe93b51Bfz5PMRMihVf1iVkl82ePQG7V5vwrhzB11v0NtsR25PSZRGiSomJaJw=="], + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], @@ -2240,6 +2282,8 @@ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "uploadthing": ["uploadthing@7.7.4", "", { "dependencies": { "@effect/platform": "0.90.3", "@standard-schema/spec": "1.0.0-beta.4", "@uploadthing/mime-types": "0.3.6", "@uploadthing/shared": "7.1.10", "effect": "3.17.7" }, "peerDependencies": { "express": "*", "h3": "*", "tailwindcss": "^3.0.0 || ^4.0.0-beta.0" }, "optionalPeers": ["express", "h3", "tailwindcss"] }, "sha512-rlK/4JWHW5jP30syzWGBFDDXv3WJDdT8gn9OoxRJmXLoXi94hBmyyjxihGlNrKhBc81czyv8TkzMioe/OuKGfA=="], + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], @@ -2442,6 +2486,8 @@ "@uiw/react-markdown-preview/rehype-prism-plus": ["rehype-prism-plus@2.0.0", "", { "dependencies": { "hast-util-to-string": "^3.0.0", "parse-numeric-range": "^1.3.0", "refractor": "^4.8.0", "rehype-parse": "^9.0.0", "unist-util-filter": "^5.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ=="], + "@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -2462,6 +2508,8 @@ "cssstyle/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + "effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -2480,6 +2528,8 @@ "jest-circus/pretty-format": ["pretty-format@30.3.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ=="], + "jest-circus/pure-rand": ["pure-rand@7.0.1", "", {}, "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ=="], + "jest-config/pretty-format": ["pretty-format@30.3.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ=="], "jest-diff/pretty-format": ["pretty-format@30.3.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ=="], diff --git a/package.json b/package.json index 4a44864..bb65e74 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@trpc/react-query": "^11.12.0", "@trpc/server": "^11.12.0", "@uiw/react-md-editor": "^4.0.11", + "@uploadthing/react": "^7.3.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -94,6 +95,7 @@ "tailwind-merge": "^3.5.0", "tailwindcss-motion": "^1.1.1", "type-fest": "^5.4.4", + "uploadthing": "^7.7.4", "vaul": "^1.1.2", "zod": "^4.3.6" }, diff --git a/src/app/_components/Animated/AnimateIn.tsx b/src/app/_components/Animated/AnimateIn.tsx new file mode 100644 index 0000000..6741860 --- /dev/null +++ b/src/app/_components/Animated/AnimateIn.tsx @@ -0,0 +1,30 @@ +import { useGSAP } from "@gsap/react"; +import { useRef, type ReactNode } from "react"; +import { useGsapContext } from "~/app/_providers/GsapProvicer"; +import { SplitText } from "gsap/SplitText"; +import gsap from 'gsap' +const AnimateTextIn = ({children,animation="type",position}:{children:ReactNode,animation?:"type"|"slide",position:gsap.Position}) => { + const el = useRef(null) + const gsapContext = useGsapContext(); + useGSAP(() => { + const rect = el.current?.getBoundingClientRect() + const isInView = rect && rect.top < window.innerHeight + const chars = new SplitText(el.current,{type:'chars'}) + gsapContext?.addAnimation(gsap.to(el.current,{opacity:100, duration:0}),0) + const fromVars = animation === "slide" + ? {opacity:0, x:-10, duration: 0.2, stagger: {each: 0.08}, ease:'bounce.inOut'} + : {opacity:0, duration: 0.01, stagger: {each: 0.04}, ease: 'bounce.inOut'} + if (isInView) { + gsapContext?.addAnimation(gsap.from(chars.chars, fromVars),position) + } else { + gsap.from(chars.chars, { ...fromVars, scrollTrigger: { trigger: el.current, start: 'top 85%', scroller: gsapContext?.getScroller() } }) + } + }, { dependencies: [] }) + return ( +
+ {children} +
+ ) +} + +export default AnimateTextIn; diff --git a/src/app/_components/Animated/AnimatedBackGroundContainer.tsx b/src/app/_components/Animated/AnimatedBackGroundContainer.tsx new file mode 100644 index 0000000..76eb714 --- /dev/null +++ b/src/app/_components/Animated/AnimatedBackGroundContainer.tsx @@ -0,0 +1,302 @@ +"use client"; + +import React, { useRef, useEffect, useCallback, useState } from "react"; +import { useGSAP } from "@gsap/react"; +import gsap from "gsap"; +import { useTheme } from "next-themes"; + +/* ───────────────────────────────────────────── + * Config — grayscale palettes + * ───────────────────────────────────────────── */ +const PALETTES = { + dark: { + base: "#0a0a0a", + particles: [ + "rgba(255,255,255,0.70)", + "rgba(255,255,255,0.45)", + "rgba(180,180,180,0.50)", + "rgba(200,200,200,0.35)", + "rgba(255,255,255,0.22)", + ], + }, + light: { + base: "#f5f5f5", + particles: [ + "rgba(0,0,0,0.55)", + "rgba(0,0,0,0.35)", + "rgba(60,60,60,0.40)", + "rgba(80,80,80,0.25)", + "rgba(0,0,0,0.18)", + ], + }, +} as const; + +/* ───────────────────────────────────────────── + * Helpers + * ───────────────────────────────────────────── */ +const isMobileDevice = (): boolean => { + if (typeof window === "undefined") return false; + return window.matchMedia("(pointer: coarse)").matches || window.innerWidth < 768; +}; + +const rand = (min: number, max: number) => Math.random() * (max - min) + min; + +/* ───────────────────────────────────────────── + * Particle + * ───────────────────────────────────────────── */ +interface Particle { + angle: number; + radius: number; + speed: number; + size: number; + colorIndex: number; + wobbleAmp: number; + wobbleSpeed: number; + wobblePhase: number; +} + +const spawnParticle = (): Particle => ({ + angle: rand(0, Math.PI * 2), + radius: rand(30, 240), + speed: rand(0.003, 0.002) * (Math.random() > 0.5 ? 1 : -1), + size: rand(1.2, 4), + colorIndex: Math.floor(rand(0, 5)), + wobbleAmp: rand(6, 30), + wobbleSpeed: rand(0.008, 0.035), + wobblePhase: rand(0, Math.PI * 2), +}); + +/* ───────────────────────────────────────────── + * Component + * ───────────────────────────────────────────── */ +interface AnimatedBackgroundContainerProps { + children: React.ReactNode; + className?: string; + /** Number of orbiting particles. Default 60 */ + particleCount?: number; + /** Max orbit radius in px — controls how far particles spread from the cursor. Default 240 */ + orbitRadius?: number; + /** How quickly particles catch up to cursor (0–1). Default 0.06 */ + followSpeed?: number; + /** Speed multiplier for mobile random anchor drift. Default 1 */ + mobileSpeed?: number; +} + +export default function AnimatedBackgroundContainer({ + children, + className = "", + particleCount = 60, + orbitRadius = 240, + followSpeed = 0.06, + mobileSpeed = 1, +}: AnimatedBackgroundContainerProps) { + const canvasRef = useRef(null); + const containerRef = useRef(null); + const mousePos = useRef({ x: 0, y: 0 }); + const smoothMouse = useRef({ x: 0, y: 0 }); + const mobileAnchor = useRef({ x: 0, y: 0 }); + const mobileTarget = useRef({ x: 0, y: 0 }); + const isMobile = useRef(false); + const particles = useRef([]); + const frame = useRef(0); + const [mounted, setMounted] = useState(false); + + const { resolvedTheme } = useTheme(); + let isDark = resolvedTheme === "dark"; + if (resolvedTheme == undefined) { + isDark = true; + } + const palette = isDark ? PALETTES.dark : PALETTES.light; + + /* Spawn particles */ + useEffect(() => { + const minR = Math.max(10, orbitRadius * 0.12); + particles.current = Array.from({ length: particleCount }, () => ({ + ...spawnParticle(), + radius: rand(minR, orbitRadius), + wobbleAmp: rand(orbitRadius * 0.025, orbitRadius * 0.12), + })); + }, [particleCount, orbitRadius]); + + /* Detect mobile & seed positions */ + useEffect(() => { + setMounted(true); + isMobile.current = isMobileDevice(); + if (containerRef.current) { + const cx = containerRef.current.clientWidth / 2; + const cy = containerRef.current.clientHeight / 2; + mousePos.current = { x: cx, y: cy }; + smoothMouse.current = { x: cx, y: cy }; + mobileAnchor.current = { x: cx, y: cy }; + mobileTarget.current = { + x: rand(cx * 0.4, cx * 1.6), + y: rand(cy * 0.4, cy * 1.6), + }; + } + }, []); + + /* Resize canvas to match container */ + useEffect(() => { + const resize = () => { + const canvas = canvasRef.current; + const container = containerRef.current; + if (!canvas || !container) return; + const dpr = window.devicePixelRatio || 1; + const w = container.clientWidth; + const h = container.clientHeight; + canvas.width = w * dpr; + canvas.height = h * dpr; + canvas.style.width = `${w}px`; + canvas.style.height = `${h}px`; + const ctx = canvas.getContext("2d"); + if (ctx) ctx.scale(dpr, dpr); + }; + resize(); + window.addEventListener("resize", resize); + return () => window.removeEventListener("resize", resize); + }, []); + + /* Mouse tracking (desktop only) */ + const handleMouseMove = useCallback((e: MouseEvent) => { + if (!containerRef.current || isMobile.current) return; + const rect = containerRef.current.getBoundingClientRect(); + mousePos.current = { + x: e.clientX - rect.left, + y: e.clientY - rect.top, + }; + }, []); + + useEffect(() => { + const el = containerRef.current; + if (!el) return; + el.addEventListener("mousemove", handleMouseMove, { passive: true }); + return () => el.removeEventListener("mousemove", handleMouseMove); + }, [handleMouseMove]); + + /* ── GSAP ticker — draw loop ── */ + useGSAP( + () => { + if (!mounted) return; + const canvas = canvasRef.current; + const container = containerRef.current; + if (!canvas || !container) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const tick = () => { + const w = container.clientWidth; + const h = container.clientHeight; + frame.current++; + + /* Anchor: smooth-follow cursor or drift on mobile */ + if (isMobile.current) { + mobileAnchor.current.x += + (mobileTarget.current.x - mobileAnchor.current.x) * 0.008 * mobileSpeed; + mobileAnchor.current.y += + (mobileTarget.current.y - mobileAnchor.current.y) * 0.008 * mobileSpeed; + + const dx = mobileTarget.current.x - mobileAnchor.current.x; + const dy = mobileTarget.current.y - mobileAnchor.current.y; + if (Math.sqrt(dx * dx + dy * dy) < 30) { + mobileTarget.current = { + x: rand(w * 0.15, w * 0.85), + y: rand(h * 0.15, h * 0.85), + }; + } + smoothMouse.current.x = mobileAnchor.current.x; + smoothMouse.current.y = mobileAnchor.current.y; + } else { + smoothMouse.current.x += + (mousePos.current.x - smoothMouse.current.x) * followSpeed; + smoothMouse.current.y += + (mousePos.current.y - smoothMouse.current.y) * followSpeed; + } + + const cx = smoothMouse.current.x; + const cy = smoothMouse.current.y; + + /* Clear frame */ + ctx.clearRect(0, 0, w, h); + + /* Draw each particle */ + particles.current.forEach((p) => { + p.angle += p.speed; + + const wobble = + Math.sin(frame.current * p.wobbleSpeed + p.wobblePhase) * p.wobbleAmp; + const r = p.radius + wobble; + + const x = cx + Math.cos(p.angle) * r; + const y = cy + Math.sin(p.angle) * r; + + /* Soft fade near viewport edges */ + const edgeFade = Math.max( + 0, + Math.min(x / 80, (w - x) / 80, y / 80, (h - y) / 80, 1), + ); + if (edgeFade <= 0) return; + + ctx.globalAlpha = edgeFade; + ctx.fillStyle = palette.particles[p.colorIndex]; + ctx.beginPath(); + ctx.arc(x, y, p.size, 0, Math.PI * 2); + ctx.fill(); + }); + + ctx.globalAlpha = 1; + }; + + gsap.ticker.add(tick); + return () => { + gsap.ticker.remove(tick); + }; + }, + { + scope: containerRef, + dependencies: [mounted, isDark, followSpeed, mobileSpeed, orbitRadius, palette], + }, + ); + + /* ── Render ── */ + return ( +
+ + + {/* Grain texture */} +
+ +
{children}
+
+ ); +} diff --git a/src/app/_components/Animated/AnimatedPageTitle.tsx b/src/app/_components/Animated/AnimatedPageTitle.tsx new file mode 100644 index 0000000..43b650a --- /dev/null +++ b/src/app/_components/Animated/AnimatedPageTitle.tsx @@ -0,0 +1,23 @@ +import { useGSAP } from "@gsap/react"; import { useEffect, useLayoutEffect, useRef } from "react"; +import { useGsapContext } from "~/app/_providers/GsapProvicer"; +import { SplitText } from "gsap/SplitText"; +import gsap from 'gsap' +const AnimatedPageTitle = ( + { text, position }: { text: string, position:gsap.Position } +) => { + const el = useRef(null) + const gsapContext = useGsapContext(); + useEffect(() => { + console.log("add animated title with:",position) + const split = new SplitText(el.current, { type: "chars" }) + gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100 }),position) + gsapContext?.addAnimation(gsap.from(split.chars, { + stagger: 0.05, rotate: -90, opacity: 0, x: -10 + }),'>') + },[]) + return ( +

{text}

+ ) +} + +export default AnimatedPageTitle; diff --git a/src/app/_components/ThemeIcon.tsx b/src/app/_components/ThemeIcon.tsx index 4162143..4b66d5c 100644 --- a/src/app/_components/ThemeIcon.tsx +++ b/src/app/_components/ThemeIcon.tsx @@ -4,10 +4,18 @@ import { Moon, Sun } from "lucide-react" import { useEffect } from "react" type Props = {activeTheme:string|undefined} const ThemeIcon = (props:Props) => { - if (props.activeTheme == "dark") { - return () - } else { - return () - } + return ( + <> + {props.activeTheme && props.activeTheme == 'dark' && + + } + {props.activeTheme && props.activeTheme == 'light' && + + } + {!props.activeTheme && + + } + + ) } export default ThemeIcon; diff --git a/src/app/_components/ThemeSwitch.tsx b/src/app/_components/ThemeSwitch.tsx index c490947..df26e9a 100644 --- a/src/app/_components/ThemeSwitch.tsx +++ b/src/app/_components/ThemeSwitch.tsx @@ -8,6 +8,9 @@ import ThemeIcon from "./ThemeIcon" export function ThemeSwitch() { const { setTheme, theme } = useTheme() + if (!theme) { + setTheme('dark') + } const toggleTheme = () => { setTheme(theme == "dark" ? "light" : "dark") } diff --git a/src/app/_components/TopNav.tsx b/src/app/_components/TopNav.tsx index 34f797a..f9f4716 100644 --- a/src/app/_components/TopNav.tsx +++ b/src/app/_components/TopNav.tsx @@ -19,6 +19,9 @@ export default function TopNav() { +
diff --git a/src/app/_providers/GsapProvicer.tsx b/src/app/_providers/GsapProvicer.tsx index fabb502..031cb98 100644 --- a/src/app/_providers/GsapProvicer.tsx +++ b/src/app/_providers/GsapProvicer.tsx @@ -1,18 +1,82 @@ 'use client' import { useGSAP } from '@gsap/react' import gsap from 'gsap' -import { createContext, useContext, type ReactNode } from 'react' +import { SplitText } from 'gsap/SplitText' +import { ScrollTrigger } from 'gsap/all' +import { createContext, useCallback, useContext, useEffect, useRef, type ReactNode } from 'react' gsap.registerPlugin(useGSAP) -const GsapContext = createContext(null) +gsap.registerPlugin(ScrollTrigger) +gsap.registerPlugin(SplitText) +const GsapContext = createContext<{ + addAnimation: ( + animation: gsap.core.TimelineChild, + position: gsap.Position + ) => void, + resetTimeline: () => void, + resumeTimeline: () => void, + getScroller: () => Element | Window | null +} | null>(null) export function useGsapContext() { return useContext(GsapContext) } -export default function GsapProvider({children}:{children:ReactNode}) { +export const useTimeLine = (dep:any,all?:boolean) => { + const gsapContext = useGsapContext() + useEffect(() => { + if (dep instanceof Array && all) { + let acc = true; + let allDepsSatisfied = dep.reduce((p,c) => c !== undefined && p ,acc ) + if (allDepsSatisfied) { + gsapContext?.resumeTimeline() + } + } else { + if (dep) { + gsapContext?.resumeTimeline() + } + } + },[dep]) + useEffect(() => { + return () => { + gsapContext?.resetTimeline() + } + },[]) +} + +export default function GsapProvider({ children }: { children: ReactNode }) { + const tl = useRef(null) + const scrollerRef = useRef(null) + const getScroller = useCallback(() => { + const cached = scrollerRef.current + if (!cached || (cached instanceof Element && !document.contains(cached))) { + scrollerRef.current = document.querySelector('[data-slot="scroll-area-viewport"]') ?? window + } + return scrollerRef.current + }, []) + useGSAP(() => { + if (!tl.current) { + tl.current = gsap.timeline({ paused: true }) + } + return () => { console.log("gsap cleanup") } + }) + + const addAnimation = useCallback((animation: gsap.core.TimelineChild, position: gsap.Position) => { + console.log("add animation to:", position, tl.current !== undefined) + tl.current?.add(animation, position); + },[]) + const resetTimeline = useCallback(() => { + tl.current?.kill() + tl.current?.revert() + ScrollTrigger.getAll().forEach(st => st.kill()) + tl.current = gsap.timeline({paused:true}) + },[]) + const resumeTimeline = useCallback(() => { + console.log("resuming timeline:",tl.current) + tl.current?.resume() + },[]) return ( - + {children} ) diff --git a/src/app/_providers/ThemeProvider.tsx b/src/app/_providers/ThemeProvider.tsx index f17ca88..901925c 100644 --- a/src/app/_providers/ThemeProvider.tsx +++ b/src/app/_providers/ThemeProvider.tsx @@ -1,23 +1,10 @@ 'use client' -import * as React from "react" import { ThemeProvider as NextThemesProvider } from "next-themes" export default function ThemeProvider({children}:{children: React.ReactNode}) { - const [mounted,setMounted] = React.useState(false) - React.useEffect(() => { - setMounted(true) - }) - if (mounted) { - return ( - - {children} - - ) - } else { - return ( - <> - {children} - - ) - } + return ( + + {children} + + ) } diff --git a/src/app/admin/_components/AdminSideBar.tsx b/src/app/admin/_components/AdminSideBar.tsx index 59f7f35..4de3ee5 100644 --- a/src/app/admin/_components/AdminSideBar.tsx +++ b/src/app/admin/_components/AdminSideBar.tsx @@ -20,6 +20,9 @@ export default async function AdminSideBar() { Create Stack Project List + + Manage Music + Some Blog Action diff --git a/src/app/admin/cv/category/_components/CreateUpdateForm.tsx b/src/app/admin/cv/category/_components/CreateUpdateForm.tsx index be4bf3b..29a1a5d 100644 --- a/src/app/admin/cv/category/_components/CreateUpdateForm.tsx +++ b/src/app/admin/cv/category/_components/CreateUpdateForm.tsx @@ -14,13 +14,13 @@ import { SelectItem } from '~/components/ui/select'; import {FormMutationContextProvider} from '~/app/_components/Form/Components/MutationProvider'; export default function CreateUpdateCvCategoryForm(params: { className?: string, entity?: IterableElement }) { const schemas = entitySchemas('cvCategory') - const [id, setId] = useState(params.entity ? params.entity.id : undefined) + const [id, setId] = useState(params.entity?.id) const form = useForm>({ resolver: zodResolver(schemas.insert), defaultValues: { - id: params.entity ? params.entity.id : crypto.randomUUID(), - name: params.entity ? params.entity.name : "", - layoutPosition: params.entity ? params.entity.layoutPosition : "col1" + id: params.entity?.id || crypto.randomUUID(), + name: params.entity?.name || "", + layoutPosition: params.entity?.layoutPosition || "col1" } }) let path = usePathname() diff --git a/src/app/admin/music/_components/UploadMusicForm.tsx b/src/app/admin/music/_components/UploadMusicForm.tsx new file mode 100644 index 0000000..ca6d70e --- /dev/null +++ b/src/app/admin/music/_components/UploadMusicForm.tsx @@ -0,0 +1,105 @@ +'use client' + +import { useState } from "react"; +import { trpc } from "~/app/_trpc/Client"; +import { UploadDropzone } from "~/lib/uploadthing"; +import { Label } from "~/components/ui/label"; +import { FormScaffold } from "~/app/_components/Form/Components"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import type { RouterOutputs } from "~/server/routers/_app"; +import type { IterableElement } from "type-fest"; +import { Toaster } from "~/components/ui/sonner"; +import { toast } from "sonner"; +import { FormMutationContextProvider } from "~/app/_components/Form/Components/MutationProvider"; +import { TextInputFormField } from "~/app/_components/Form/Fields"; +import { createMusicInputSchema } from "~/lib/trpc/music/schemas"; +export default function CreateUpdateMusicForm(props: { + entity?: IterableElement, + className?: string +}) { + const entity = props.entity; + const [id, setId] = useState(entity?.id) + const utils = trpc.useUtils(); + const form = useForm>({ + resolver: zodResolver(createMusicInputSchema), + defaultValues: { + id: entity?.id || crypto.randomUUID(), + title: entity?.title || "", + description: entity?.description || "", + fileUrl: entity?.fileUrl, + fileKey: entity?.fileKey, + fileName: entity?.fileName, + } + }) + + const createMutation = trpc.music.create.useMutation({ + onSuccess: (values) => { + setId(values?.id); + utils.music.list.invalidate(); + }, + onError: (e) => { + toast(e.message) + } + }) + const updateMutation = trpc.music.update.useMutation({ + onSuccess: (_) => { + utils.music.list.invalidate(); + }, + onError: (e) => { + toast(e.message) + } + }) + const deleteMutation = trpc.music.delete.useMutation({ + onSuccess: (_) => { + utils.music.list.invalidate(); + }, + onError: (e) => { + toast(e.message) + } + }) + + function onSubmit(values: z.infer) { + id ? + updateMutation.mutate(values) : + createMutation.mutate(values) + } + + return ( + <> + + + + + +
+ + { + toast(e.message) + }} + onClientUploadComplete={(res) => { + console.log(res) + if (res[0]) { + form.setValue('fileKey',res[0].serverData.fileKey); + form.setValue('fileName',res[0].serverData.fileName); + form.setValue('title',res[0].serverData.fileName); + form.setValue('description',res[0].serverData.fileName); + form.setValue('fileUrl',res[0].serverData.fileUrl); + } + console.log(form.getValues()); + }} + /> +
+
+
+ + ); +} diff --git a/src/app/admin/music/page.tsx b/src/app/admin/music/page.tsx new file mode 100644 index 0000000..3297f93 --- /dev/null +++ b/src/app/admin/music/page.tsx @@ -0,0 +1,26 @@ +'use client' + +import { trpc } from "~/app/_trpc/Client"; +import * as Card from "~/components/ui/card"; +import UploadMusicForm from "./_components/UploadMusicForm"; +import { CollapsibleForm } from "~/app/_components/Form/Components"; +import { useEffect } from "react"; + +export default function AdminMusicPage() { + const { data: tracks } = trpc.music.list.useQuery(); + useEffect(() => {console.log(tracks)}, [tracks]) + return ( +
+ {tracks && <> + {tracks.map((t) => ( + + + + + + ))} + } + +
+ ); +} diff --git a/src/app/api/uploadthing/route.ts b/src/app/api/uploadthing/route.ts new file mode 100644 index 0000000..6de1dee --- /dev/null +++ b/src/app/api/uploadthing/route.ts @@ -0,0 +1,6 @@ +import { createRouteHandler } from "uploadthing/next"; +import { fileRouter } from "~/server/uploadthing"; + +export const { GET, POST } = createRouteHandler({ + router: fileRouter, +}); diff --git a/src/app/cv/page.tsx b/src/app/cv/page.tsx index 1fa4154..7d3f369 100644 --- a/src/app/cv/page.tsx +++ b/src/app/cv/page.tsx @@ -1,17 +1,18 @@ 'use client' import { useGSAP } from "@gsap/react"; -import { useGsapContext } from "../_providers/GsapProvicer"; +import { useGsapContext,useTimeLine } from "../_providers/GsapProvicer"; import { trpc } from "../_trpc/Client"; import { useRef } from "react"; import { SidebarContent, SidebarProvider, Sidebar } from "~/components/ui/sidebar"; import SidebarTriggerDisappearsOnMobile from "./_components/SidebarTriggerDisappearsOnMobile"; import CvCategory from "./_components/CvCategory"; +import gsap from 'gsap' export default function CvPage() { const sidebarCategories = trpc.categoryv2.listByLayoutPosition.useQuery("sidebar"); const col1Categories = trpc.categoryv2.listByLayoutPosition.useQuery("col1"); const headerCategories = trpc.categoryv2.listByLayoutPosition.useQuery("header"); const col2Categories = trpc.categoryv2.listByLayoutPosition.useQuery("col2"); - const gsap = useGsapContext() + const gsapContext = useGsapContext() const container = useRef(null) enum Direction { Left = 1, @@ -31,12 +32,12 @@ export default function CvPage() { return { y: 100, opacity: 0, duration: 0.5 } } } + useTimeLine(col2Categories) useGSAP(() => { const items = gsap?.utils.toArray('.gsapan'); - const tl = gsap?.timeline(); let dir = Direction.Left; items?.forEach(item => { - tl?.from(item, nextGsapConf(dir)) + gsapContext?.addAnimation(gsap.from(item, nextGsapConf(dir)),0) if (dir == Direction.Down) { dir = Direction.Left } else { @@ -47,7 +48,7 @@ export default function CvPage() { return ( <> - {(sidebarCategories.data?.length ? sidebarCategories.data?.length : 0) > 0 ? + {sidebarCategories.data && <> @@ -61,8 +62,7 @@ export default function CvPage() { })} - : - <> + }
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 520c49f..6600e68 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -12,6 +12,7 @@ import ThemeProvider from './_providers/ThemeProvider' import GsapProvider from "./_providers/GsapProvicer"; import { CodeHighlightStyle } from "./_components/CodeHighlightSyle"; import { cn } from "~/lib/utils"; +import AnimatedBackGroundContainer from "./_components/Animated/AnimatedBackGroundContainer"; const inter = Inter({ subsets: ['latin'], variable: '--font-sans' }); @@ -28,7 +29,6 @@ const geist = Geist({ variable: "--font-geist-sans", }); - export default async function RootLayout({ children, modal @@ -44,11 +44,13 @@ export default async function RootLayout({ +
{children}
{modal} +
diff --git a/src/app/music/page.tsx b/src/app/music/page.tsx new file mode 100644 index 0000000..ce082bb --- /dev/null +++ b/src/app/music/page.tsx @@ -0,0 +1,54 @@ +'use client' +import { trpc } from "~/app/_trpc/Client"; +import * as Card from "~/components/ui/card"; +import { useTimeLine } from "../_providers/GsapProvicer"; +import AnimatedPageTitle from "../_components/Animated/AnimatedPageTitle"; +import { Spinner } from "~/components/ui/spinner"; +import AnimateTextIn from "../_components/Animated/AnimateIn"; +import { ScrollArea } from "~/components/ui/scroll-area"; +export default function MusicPage() { + const { data: tracks, isLoading } = trpc.music.list.useQuery(); + useTimeLine(tracks) + return ( + + + +
+

All works on this page are licensed under:

+ CC BY-NC-SA 4.0 +
+ + + + +
+
+
+ {tracks && tracks.map((track, i) => ( + + + + {track.title} + + + + {track.description && ( +

{track.description}

+ )} + +
+
+ ))} + {!isLoading && !tracks?.length && +
+ No music yet. +
+ } + {isLoading &&
+ Loading Tracks +
} +
+ ); +} diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 614b60f..2160864 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -1,5 +1,7 @@ -import * as React from "react" - +import { useGSAP } from "@gsap/react";import * as React from "react" +import { useRef } from "react"; +import { useGsapContext } from "~/app/_providers/GsapProvicer"; +import gsap from 'gsap' import { cn } from "~/lib/utils" function Card({ @@ -12,7 +14,41 @@ function Card({ data-slot="card" data-size={size} className={cn( - "group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", + "group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl bg-opacity-60 backdrop-blur-sm", + className + )} + {...props} + /> + ) +} + +function AnimatedCard({ + className, + position = 0, + size = "default", + ...props +}: React.ComponentProps<"div"> & { size?: "default" | "sm", position: gsap.Position }) { + const gsapContext = useGsapContext() + const ref = useRef(null) + useGSAP(() => { + const rect = ref.current?.getBoundingClientRect() + const isInView = rect && rect.top < window.innerHeight + const fromVars = { x: -100, opacity: 0, duration: 0.5 } + if (isInView) { + gsapContext?.addAnimation(gsap.from(ref.current, fromVars), position) + } else { + const scroller = gsapContext?.getScroller() + console.log('scroller:', scroller) + gsap.from(ref.current, { ...fromVars, scrollTrigger: { trigger: ref.current, start: 'top 85%', scroller } }) + } + }, { dependencies: [] }) + return ( +
img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl bg-opacity-60 backdrop-blur-sm", className )} {...props} @@ -100,4 +136,5 @@ export { CardAction, CardDescription, CardContent, + AnimatedCard } diff --git a/src/components/ui/spinner.tsx b/src/components/ui/spinner.tsx new file mode 100644 index 0000000..6e18a62 --- /dev/null +++ b/src/components/ui/spinner.tsx @@ -0,0 +1,10 @@ +import { cn } from "~/lib/utils" +import { Loader2Icon } from "lucide-react" + +function Spinner({ className, ...props }: React.ComponentProps<"svg">) { + return ( + + ) +} + +export { Spinner } diff --git a/src/env.js b/src/env.js index 2b0220e..3707b84 100644 --- a/src/env.js +++ b/src/env.js @@ -27,6 +27,7 @@ export const env = createEnv({ CLERK_SECRET_KEY: z.string(), ADMIN_USER_CLERK_ID: z.string(), + UPLOADTHING_TOKEN: z.string(), NODE_ENV: z .enum(["development", "test", "production"]) .default("development"), @@ -64,6 +65,7 @@ export const env = createEnv({ POSTGRES_URL_NO_SSL: process.env.POSTGRES_URL_NO_SSL, POSTGRES_PRISMA_URL: process.env.POSTGRES_PRISMA_URL, ADMIN_USER_CLERK_ID: process.env.ADMIN_USER_CLERK_ID, + UPLOADTHING_TOKEN: process.env.UPLOADTHING_TOKEN, NEXT_PUBLIC_ADMIN_USER_CLERK_ID: process.env.NEXT_PUBLIC_ADMIN_USER_CLERK_ID, CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY, NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, diff --git a/src/lib/trpc/music/schemas.ts b/src/lib/trpc/music/schemas.ts new file mode 100644 index 0000000..31bfb76 --- /dev/null +++ b/src/lib/trpc/music/schemas.ts @@ -0,0 +1,18 @@ +import z from "zod" + +export const createMusicInputSchema = z.object({ + id: z.string().uuid(), + title: z.string().min(1).max(100), + description: z.string().optional(), + fileUrl: z.string(), + fileKey: z.string(), + fileName: z.string(), + }) +export const updateMusicInputSchema = z.object({ + id: z.string().uuid(), + title: z.string().min(1).max(100).optional(), + description: z.string().optional(), + fileUrl: z.string().optional(), + fileKey: z.string().optional(), + fileName: z.string().optional(), + }) diff --git a/src/lib/uploadthing.ts b/src/lib/uploadthing.ts new file mode 100644 index 0000000..ff10599 --- /dev/null +++ b/src/lib/uploadthing.ts @@ -0,0 +1,5 @@ +import { generateUploadButton, generateUploadDropzone } from "@uploadthing/react"; +import type { FileRouter } from "~/server/uploadthing"; + +export const UploadButton = generateUploadButton(); +export const UploadDropzone = generateUploadDropzone(); diff --git a/src/server/dbschema/schema.ts b/src/server/dbschema/schema.ts index 7d6239c..c09696c 100644 --- a/src/server/dbschema/schema.ts +++ b/src/server/dbschema/schema.ts @@ -85,3 +85,21 @@ export const techStack = createTable( stackItems: stackItemEnum().array() }) ) + +export const music = createTable( + "music", + (d) => ({ + id: d.uuid().primaryKey().notNull(), + title: d.varchar({ length: 100 }).notNull(), + description: d.text(), + fileUrl: d.varchar("file_url", { length: 500 }).notNull(), + fileKey: d.varchar("file_key", { length: 200 }).notNull(), + fileName: d.varchar("file_name", { length: 200 }).notNull(), + createdAt: d + .timestamp({ withTimezone: true }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull() + .$type(), + updatedAt: d.timestamp({ withTimezone: true }).$onUpdate(() => new Date()).$type(), + }) +) diff --git a/src/server/routers/_app.ts b/src/server/routers/_app.ts index 8131279..9a5a494 100644 --- a/src/server/routers/_app.ts +++ b/src/server/routers/_app.ts @@ -5,6 +5,7 @@ import { projectRouter } from "./project"; import { techStackRouter } from "./techStack"; import { cvCategoryRouter } from "./cvCategory"; import { cvEntryRouter } from "./cvEntry"; +import { musicRouter } from "./music"; import { trpcCrudRouterFromDrizzleEntity } from "../lib"; import { cvCategory } from "../dbschema/schema"; @@ -17,6 +18,7 @@ export const trpcRouter = router({ categoryv2: cvCategoryRouter, entry: trpcCrudRouterFromDrizzleEntity('cvEntry').router, entryv2: cvEntryRouter, + music: musicRouter, }); export type TrpcRouter = typeof trpcRouter; diff --git a/src/server/routers/music.ts b/src/server/routers/music.ts new file mode 100644 index 0000000..190c3ce --- /dev/null +++ b/src/server/routers/music.ts @@ -0,0 +1,48 @@ +import { publicProcedure, router } from "~/server/trpc"; +import { db } from "~/server/db"; +import { music } from "~/server/dbschema/schema"; +import { eq } from "drizzle-orm"; +import { isAdmin } from "~/app/actions"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { createMusicInputSchema, updateMusicInputSchema } from "~/lib/trpc/music/schemas"; +import { utapi } from "../uploadthing"; +export const musicRouter = router({ + list: publicProcedure.query(async () => { + let res = await db.select().from(music).orderBy(music.createdAt); + console.log(res); + return res; + }), + create: publicProcedure + .input( + createMusicInputSchema + ) + .mutation(async ({ input }) => { + const admin = await isAdmin(); + if (!admin) throw new TRPCError({ code: "FORBIDDEN", message: "Access denied" }); + let res = await db.insert(music).values(input).returning(); + return res.at(0); + }), + update: publicProcedure + .input( + updateMusicInputSchema + ) + .mutation(async ({ input }) => { + const admin = await isAdmin(); + if (!admin) throw new TRPCError({ code: "FORBIDDEN", message: "Access denied" }); + const { id, ...data } = input; + return db.update(music).set(data).where(eq(music.id, id)).returning(); + }), + delete: publicProcedure + .input(z.object({id:z.string().uuid()})) + .mutation(async ({ input }) => { + const admin = await isAdmin(); + if (!admin) throw new TRPCError({ code: "FORBIDDEN", message: "Access denied" }); + let res = await db.delete(music).where(eq(music.id, input.id)).returning(); + let ret = res.at(0) + if (ret) { + utapi.deleteFiles(ret.fileKey) + } + return ret; + }), +}); diff --git a/src/server/uploadthing.ts b/src/server/uploadthing.ts new file mode 100644 index 0000000..c303e92 --- /dev/null +++ b/src/server/uploadthing.ts @@ -0,0 +1,22 @@ +import { createUploadthing, type FileRouter as UploadThingFileRouter } from "uploadthing/next"; +import { UTApi } from 'uploadthing/server' +import { isAdmin } from "~/app/actions"; + +const f = createUploadthing(); + +export const fileRouter = { + musicUploader: f({ audio: { maxFileSize: "64MB", maxFileCount: 1 } }) + .middleware(async () => { + const admin = await isAdmin(); + if (!admin) throw new Error("Unauthorized"); + return {}; + }) + .onUploadComplete(async ({ file }) => { + console.log(file) + return { fileUrl: file.ufsUrl, fileKey: file.key, fileName: file.name }; + }), +} satisfies UploadThingFileRouter ; + +export type FileRouter = typeof fileRouter; + +export const utapi = new UTApi(); diff --git a/src/styles/globals.css b/src/styles/globals.css index 892d058..0e5b91a 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -139,4 +139,4 @@ * { -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ -} \ No newline at end of file +}