Compare commits
1 Commits
musicpage
...
9b48661a6a
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b48661a6a |
34
README.md
34
README.md
@@ -1,29 +1,13 @@
|
||||
# Create T3 App
|
||||
# My Personal Website
|
||||
|
||||
This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`.
|
||||
## Using:
|
||||
|
||||
## What's next? How do I make an app with this?
|
||||
- nextjs
|
||||
- trpc
|
||||
- neon
|
||||
- uploadthing
|
||||
- drizzle
|
||||
- gsap
|
||||
- openai
|
||||
|
||||
We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary.
|
||||
|
||||
If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help.
|
||||
|
||||
- [Next.js](https://nextjs.org)
|
||||
- [NextAuth.js](https://next-auth.js.org)
|
||||
- [Prisma](https://prisma.io)
|
||||
- [Drizzle](https://orm.drizzle.team)
|
||||
- [Tailwind CSS](https://tailwindcss.com)
|
||||
- [tRPC](https://trpc.io)
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources:
|
||||
|
||||
- [Documentation](https://create.t3.gg/)
|
||||
- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials
|
||||
|
||||
You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome!
|
||||
|
||||
## How do I deploy this?
|
||||
|
||||
Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information.
|
||||
|
||||
54
bun.lock
54
bun.lock
@@ -49,7 +49,6 @@
|
||||
"@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",
|
||||
@@ -81,7 +80,6 @@
|
||||
"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",
|
||||
},
|
||||
@@ -274,8 +272,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -482,18 +478,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -536,8 +520,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -726,7 +708,7 @@
|
||||
|
||||
"@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.0.0-beta.4", "", {}, "sha512-d3IxtzLo7P1oZ8s8YNvxzBUXRXojSut8pbPrTYtzsc5sn4+53jVqbk66pQerSZbZSJZQux6LkclB/+8IDordHg=="],
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||
|
||||
@@ -956,12 +938,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -1238,8 +1214,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -1308,8 +1282,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -1332,14 +1304,10 @@
|
||||
|
||||
"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=="],
|
||||
@@ -1818,14 +1786,8 @@
|
||||
|
||||
"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=="],
|
||||
@@ -1848,8 +1810,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -1974,7 +1934,7 @@
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
|
||||
"pure-rand": ["pure-rand@7.0.1", "", {}, "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ=="],
|
||||
|
||||
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||
|
||||
@@ -2140,8 +2100,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -2282,8 +2240,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -2486,8 +2442,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -2508,8 +2462,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -2528,8 +2480,6 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -63,7 +63,6 @@
|
||||
"@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",
|
||||
@@ -95,7 +94,6 @@
|
||||
"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"
|
||||
},
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
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<HTMLDivElement>(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 (
|
||||
<div ref={el} className="opacity-0">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AnimateTextIn;
|
||||
@@ -1,302 +0,0 @@
|
||||
"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<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(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<Particle[]>([]);
|
||||
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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={className}
|
||||
style={{
|
||||
position: "relative",
|
||||
minHeight: "100vh",
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
transition: "background-color 0.6s ease",
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
aria-hidden
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
zIndex: 0,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Grain texture */}
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
zIndex: 1,
|
||||
opacity: isDark ? 0.05 : 0.03,
|
||||
pointerEvents: "none",
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: "repeat",
|
||||
backgroundSize: "180px 180px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ position: "relative", zIndex: 2 }}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
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<HTMLHeadingElement>(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 (
|
||||
<h1 className="text-4xl opacity-0 font-bold text-balance w-full" ref={el}> {text} </h1>
|
||||
)
|
||||
}
|
||||
|
||||
export default AnimatedPageTitle;
|
||||
@@ -4,18 +4,10 @@ import { Moon, Sun } from "lucide-react"
|
||||
import { useEffect } from "react"
|
||||
type Props = {activeTheme:string|undefined}
|
||||
const ThemeIcon = (props:Props) => {
|
||||
return (
|
||||
<>
|
||||
{props.activeTheme && props.activeTheme == 'dark' &&
|
||||
<Sun/>
|
||||
}
|
||||
{props.activeTheme && props.activeTheme == 'light' &&
|
||||
<Moon/>
|
||||
}
|
||||
{!props.activeTheme &&
|
||||
<Sun/>
|
||||
}
|
||||
</>
|
||||
)
|
||||
if (props.activeTheme == "dark") {
|
||||
return (<Sun/>)
|
||||
} else {
|
||||
return (<Moon/>)
|
||||
}
|
||||
}
|
||||
export default ThemeIcon;
|
||||
|
||||
@@ -8,9 +8,6 @@ import ThemeIcon from "./ThemeIcon"
|
||||
|
||||
export function ThemeSwitch() {
|
||||
const { setTheme, theme } = useTheme()
|
||||
if (!theme) {
|
||||
setTheme('dark')
|
||||
}
|
||||
const toggleTheme = () => {
|
||||
setTheme(theme == "dark" ? "light" : "dark")
|
||||
}
|
||||
|
||||
@@ -19,9 +19,6 @@ export default function TopNav() {
|
||||
<Button asChild className="flex h-10 lg:h-full w-full lg:w-20" variant="outline">
|
||||
<Link href={"/projects"}> Projects </Link>
|
||||
</Button>
|
||||
<Button asChild className="flex h-10 lg:h-full w-full lg:w-20" variant="outline">
|
||||
<Link href={"/music"}> Music </Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col-reverse flex-wrap lg:h-full w-20 lg:w-fit lg:flex-row lg:ml-auto">
|
||||
<AdminWrap>
|
||||
|
||||
@@ -1,82 +1,18 @@
|
||||
'use client'
|
||||
import { useGSAP } from '@gsap/react'
|
||||
import gsap from 'gsap'
|
||||
import { SplitText } from 'gsap/SplitText'
|
||||
import { ScrollTrigger } from 'gsap/all'
|
||||
import { createContext, useCallback, useContext, useEffect, useRef, type ReactNode } from 'react'
|
||||
import { createContext, useContext, type ReactNode } from 'react'
|
||||
|
||||
gsap.registerPlugin(useGSAP)
|
||||
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)
|
||||
const GsapContext = createContext<typeof globalThis.gsap | null>(null)
|
||||
|
||||
export function useGsapContext() {
|
||||
return useContext(GsapContext)
|
||||
}
|
||||
|
||||
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<gsap.core.Timeline | null>(null)
|
||||
const scrollerRef = useRef<Element | Window | null>(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()
|
||||
},[])
|
||||
export default function GsapProvider({children}:{children:ReactNode}) {
|
||||
return (
|
||||
<GsapContext.Provider value={{ addAnimation, resetTimeline, resumeTimeline, getScroller }}>
|
||||
<GsapContext.Provider value={gsap}>
|
||||
{children}
|
||||
</GsapContext.Provider>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
'use client'
|
||||
import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
|
||||
export default function ThemeProvider({children}:{children: React.ReactNode}) {
|
||||
return (
|
||||
<NextThemesProvider disableTransitionOnChange attribute="class" defaultTheme="dark">
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
)
|
||||
const [mounted,setMounted] = React.useState(false)
|
||||
React.useEffect(() => {
|
||||
setMounted(true)
|
||||
})
|
||||
if (mounted) {
|
||||
return (
|
||||
<NextThemesProvider disableTransitionOnChange nonce="test" attribute="class" defaultTheme="dark">
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,6 @@ export default async function AdminSideBar() {
|
||||
<Link href={"/admin/project/techStack/create"}> Create Stack </Link>
|
||||
<Link href={"/admin/project/list"}> Project List </Link>
|
||||
</SimpleSidebarGroup>
|
||||
<SimpleSidebarGroup lable="Music">
|
||||
<Link href={"/admin/music"}> Manage Music </Link>
|
||||
</SimpleSidebarGroup>
|
||||
<SimpleSidebarGroup lable="Blog">
|
||||
<Link href={"/"}> Some Blog Action </Link>
|
||||
</SimpleSidebarGroup>
|
||||
|
||||
@@ -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<RouterOutputs['category']['select']> }) {
|
||||
const schemas = entitySchemas('cvCategory')
|
||||
const [id, setId] = useState<string | undefined>(params.entity?.id)
|
||||
const [id, setId] = useState<string | undefined>(params.entity ? params.entity.id : undefined)
|
||||
const form = useForm<z.infer<typeof schemas.insert>>({
|
||||
resolver: zodResolver(schemas.insert),
|
||||
defaultValues: {
|
||||
id: params.entity?.id || crypto.randomUUID(),
|
||||
name: params.entity?.name || "",
|
||||
layoutPosition: params.entity?.layoutPosition || "col1"
|
||||
id: params.entity ? params.entity.id : crypto.randomUUID(),
|
||||
name: params.entity ? params.entity.name : "",
|
||||
layoutPosition: params.entity ? params.entity.layoutPosition : "col1"
|
||||
}
|
||||
})
|
||||
let path = usePathname()
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
'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<RouterOutputs['music']['list']>,
|
||||
className?: string
|
||||
}) {
|
||||
const entity = props.entity;
|
||||
const [id, setId] = useState<string | undefined>(entity?.id)
|
||||
const utils = trpc.useUtils();
|
||||
const form = useForm<z.infer<typeof createMusicInputSchema>>({
|
||||
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<typeof createMusicInputSchema>) {
|
||||
id ?
|
||||
updateMutation.mutate(values) :
|
||||
createMutation.mutate(values)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toaster />
|
||||
<FormMutationContextProvider value={{
|
||||
createMutation: createMutation,
|
||||
updateMutation: updateMutation,
|
||||
deleteMutation: deleteMutation
|
||||
}}>
|
||||
<FormScaffold form={form} onSubmit={onSubmit} title='Music' id={id} className={props.className}>
|
||||
<TextInputFormField control={form.control} name='title' label='Title'/>
|
||||
<TextInputFormField control={form.control} name='description' label='Description'/>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>Audio File</Label>
|
||||
<UploadDropzone
|
||||
endpoint="musicUploader"
|
||||
config={{mode: 'auto'}}
|
||||
onUploadError={(e) => {
|
||||
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());
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormScaffold>
|
||||
</FormMutationContextProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
'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 (
|
||||
<div className="w-5/6 lg:w-1/2 flex flex-col gap-3">
|
||||
{tracks && <>
|
||||
{tracks.map((t) => (
|
||||
<Card.Card key={t.id}>
|
||||
<Card.CardContent>
|
||||
<UploadMusicForm entity={t} className="w-full"/>
|
||||
</Card.CardContent>
|
||||
</Card.Card>
|
||||
))}
|
||||
</>}
|
||||
<CollapsibleForm entityName="Track" form={UploadMusicForm}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { createRouteHandler } from "uploadthing/next";
|
||||
import { fileRouter } from "~/server/uploadthing";
|
||||
|
||||
export const { GET, POST } = createRouteHandler({
|
||||
router: fileRouter,
|
||||
});
|
||||
@@ -1,18 +1,17 @@
|
||||
'use client'
|
||||
import { useGSAP } from "@gsap/react";
|
||||
import { useGsapContext,useTimeLine } from "../_providers/GsapProvicer";
|
||||
import { useGsapContext } 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 gsapContext = useGsapContext()
|
||||
const gsap = useGsapContext()
|
||||
const container = useRef<HTMLDivElement>(null)
|
||||
enum Direction {
|
||||
Left = 1,
|
||||
@@ -32,12 +31,12 @@ export default function CvPage() {
|
||||
return { y: 100, opacity: 0, duration: 0.5 }
|
||||
}
|
||||
}
|
||||
useTimeLine(col2Categories)
|
||||
useGSAP(() => {
|
||||
const items = gsap?.utils.toArray<GSAPTweenTarget>('.gsapan');
|
||||
const tl = gsap?.timeline();
|
||||
let dir = Direction.Left;
|
||||
items?.forEach(item => {
|
||||
gsapContext?.addAnimation(gsap.from(item, nextGsapConf(dir)),0)
|
||||
tl?.from(item, nextGsapConf(dir))
|
||||
if (dir == Direction.Down) {
|
||||
dir = Direction.Left
|
||||
} else {
|
||||
@@ -48,7 +47,7 @@ export default function CvPage() {
|
||||
return (
|
||||
<>
|
||||
<SidebarProvider ref={container}>
|
||||
{sidebarCategories.data &&
|
||||
{(sidebarCategories.data?.length ? sidebarCategories.data?.length : 0) > 0 ?
|
||||
<>
|
||||
<SidebarTriggerDisappearsOnMobile />
|
||||
<Sidebar className="gsapan ">
|
||||
@@ -62,7 +61,8 @@ export default function CvPage() {
|
||||
})}
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</>
|
||||
</> :
|
||||
<></>
|
||||
}
|
||||
<div className="h-full w-full flex flex-wrap flex-row p-4 pt-8 ">
|
||||
<div id="mainwrap" className="flex w-full flex-col gap-4 lg:px-[15vw]">
|
||||
|
||||
@@ -12,7 +12,6 @@ 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' });
|
||||
|
||||
@@ -29,6 +28,7 @@ const geist = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
});
|
||||
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
modal
|
||||
@@ -44,13 +44,11 @@ export default async function RootLayout({
|
||||
</head>
|
||||
<body className="flex flex-col bg-background text-foreground">
|
||||
<ThemeProvider>
|
||||
<AnimatedBackGroundContainer followSpeed={0.003} particleCount={100} orbitRadius={2000}>
|
||||
<TopNav />
|
||||
<main className="absolute lg:top-10 h-screen w-screen">
|
||||
{children}
|
||||
</main>
|
||||
{modal}
|
||||
</AnimatedBackGroundContainer>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
'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 (
|
||||
<ScrollArea className="w-full h-full max-w-4xl mx-auto px-4 py-8 flex flex-col gap-4">
|
||||
<AnimatedPageTitle position={0} text="Just Some Music I Made" />
|
||||
<AnimateTextIn position={0.5}>
|
||||
<div className="flex flex-col lg:flex-row h-fit content-center">
|
||||
<p className="break-after-avoid mr-[1em]">All works on this page are licensed under:</p>
|
||||
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a>
|
||||
<div className="flex flex-row">
|
||||
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" />
|
||||
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" />
|
||||
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/nc.svg" alt="" />
|
||||
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</AnimateTextIn>
|
||||
{tracks && tracks.map((track, i) => (
|
||||
<Card.AnimatedCard key={track.id} position={i + 1}>
|
||||
<Card.CardHeader>
|
||||
<AnimateTextIn position={i + 1.2} animation="slide">
|
||||
<Card.CardTitle>{track.title}</Card.CardTitle>
|
||||
</AnimateTextIn>
|
||||
</Card.CardHeader>
|
||||
<Card.CardContent className="flex flex-col gap-3">
|
||||
{track.description && (
|
||||
<p className="text-sm text-muted-foreground gsapant">{track.description}</p>
|
||||
)}
|
||||
<audio controls className="w-full player" src={track.fileUrl}>
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</Card.CardContent>
|
||||
</Card.AnimatedCard>
|
||||
))}
|
||||
{!isLoading && !tracks?.length &&
|
||||
<div className="flex justify-center items-center text-muted-foreground">
|
||||
No music yet.
|
||||
</div>
|
||||
}
|
||||
{isLoading && <div className="w-full h-full items-center flex flex-row content-center gap-4 justify-center">
|
||||
<Spinner /> Loading Tracks
|
||||
</div>}
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
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 * as React from "react"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
function Card({
|
||||
@@ -14,41 +12,7 @@ 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 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<HTMLDivElement|null>(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 (
|
||||
<div
|
||||
ref={ref}
|
||||
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 bg-opacity-60 backdrop-blur-sm",
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -136,5 +100,4 @@ export {
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
AnimatedCard
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { cn } from "~/lib/utils"
|
||||
import { Loader2Icon } from "lucide-react"
|
||||
|
||||
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
||||
return (
|
||||
<Loader2Icon role="status" aria-label="Loading" className={cn("size-4 animate-spin", className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Spinner }
|
||||
@@ -27,7 +27,6 @@ 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"),
|
||||
@@ -65,7 +64,6 @@ 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,
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
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(),
|
||||
})
|
||||
@@ -1,5 +0,0 @@
|
||||
import { generateUploadButton, generateUploadDropzone } from "@uploadthing/react";
|
||||
import type { FileRouter } from "~/server/uploadthing";
|
||||
|
||||
export const UploadButton = generateUploadButton<FileRouter>();
|
||||
export const UploadDropzone = generateUploadDropzone<FileRouter>();
|
||||
@@ -85,21 +85,3 @@ 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<Date>(),
|
||||
updatedAt: d.timestamp({ withTimezone: true }).$onUpdate(() => new Date()).$type<Date>(),
|
||||
})
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ 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";
|
||||
|
||||
@@ -18,7 +17,6 @@ export const trpcRouter = router({
|
||||
categoryv2: cvCategoryRouter,
|
||||
entry: trpcCrudRouterFromDrizzleEntity('cvEntry').router,
|
||||
entryv2: cvEntryRouter,
|
||||
music: musicRouter,
|
||||
});
|
||||
|
||||
export type TrpcRouter = typeof trpcRouter;
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
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;
|
||||
}),
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user