Compare commits
1 Commits
c5b3ee3875
...
projectpag
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ef0b27c50 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -46,5 +46,4 @@ yarn-error.log*
|
||||
.idea
|
||||
# clerk configuration (can include secrets)
|
||||
/.clerk/
|
||||
.worktrees
|
||||
.claudesession
|
||||
|
||||
34
README.md
34
README.md
@@ -1,13 +1,29 @@
|
||||
# My Personal Website
|
||||
# Create T3 App
|
||||
|
||||
## Using:
|
||||
This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`.
|
||||
|
||||
- nextjs
|
||||
- trpc
|
||||
- neon
|
||||
- uploadthing
|
||||
- drizzle
|
||||
- gsap
|
||||
- openai
|
||||
## What's next? How do I make an app with this?
|
||||
|
||||
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.
|
||||
|
||||
112
bun.lock
112
bun.lock
@@ -5,8 +5,6 @@
|
||||
"": {
|
||||
"name": "gregorlohaus.com",
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^3.0.41",
|
||||
"@ai-sdk/react": "^3.0.118",
|
||||
"@clerk/nextjs": "^7.0.2",
|
||||
"@electric-sql/pglite": "^0.3.16",
|
||||
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||
@@ -51,8 +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",
|
||||
"ai": "^6.0.116",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -62,7 +58,6 @@
|
||||
"drizzle-zod": "^0.8.3",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"glazejs": "^2.0.1",
|
||||
"googleapis": "^171.4.0",
|
||||
"gsap": "^3.14.2",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.577.0",
|
||||
@@ -85,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",
|
||||
},
|
||||
@@ -124,16 +118,6 @@
|
||||
|
||||
"@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
|
||||
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.66", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A=="],
|
||||
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-IZ42A+FO+vuEQCVNqlnAPYQnnUpUfdJIwn1BEDOBywiEHa23fw7PahxVtlX9zm3/zMvTW4JKPzWyvAgDu+SQ2A=="],
|
||||
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
|
||||
|
||||
"@ai-sdk/react": ["@ai-sdk/react@3.0.118", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.19", "ai": "6.0.116", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-fBAix8Jftxse6/2YJnOFkwW1/O6EQK4DK68M9DlFmZGAzBmsaHXEPVS77sVIlkaOWCy11bE7434NAVXRY+3OsQ=="],
|
||||
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
"@antfu/ni": ["@antfu/ni@25.0.0", "", { "dependencies": { "ansis": "^4.0.0", "fzf": "^0.5.2", "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" }, "bin": { "na": "bin/na.mjs", "ni": "bin/ni.mjs", "nr": "bin/nr.mjs", "nci": "bin/nci.mjs", "nlx": "bin/nlx.mjs", "nun": "bin/nun.mjs", "nup": "bin/nup.mjs" } }, "sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA=="],
|
||||
@@ -288,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=="],
|
||||
@@ -496,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=="],
|
||||
@@ -550,10 +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=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
|
||||
"@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
|
||||
@@ -742,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=="],
|
||||
|
||||
@@ -972,14 +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=="],
|
||||
|
||||
"@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
|
||||
|
||||
"@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=="],
|
||||
@@ -1006,8 +964,6 @@
|
||||
|
||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"ai": ["ai@6.0.116", "", { "dependencies": { "@ai-sdk/gateway": "3.0.66", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA=="],
|
||||
|
||||
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
@@ -1050,16 +1006,12 @@
|
||||
|
||||
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
|
||||
|
||||
"bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="],
|
||||
|
||||
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
||||
|
||||
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
|
||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||
@@ -1072,8 +1024,6 @@
|
||||
|
||||
"bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="],
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
@@ -1260,14 +1210,10 @@
|
||||
|
||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"eciesjs": ["eciesjs@0.4.18", "", { "dependencies": { "@ecies/ciphers": "^0.2.5", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.7", "@noble/hashes": "^1.8.0" } }, "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ=="],
|
||||
|
||||
"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=="],
|
||||
@@ -1336,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=="],
|
||||
@@ -1360,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=="],
|
||||
@@ -1392,10 +1332,6 @@
|
||||
|
||||
"fzf": ["fzf@0.5.2", "", {}, "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q=="],
|
||||
|
||||
"gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="],
|
||||
|
||||
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
@@ -1428,14 +1364,6 @@
|
||||
|
||||
"globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="],
|
||||
|
||||
"google-auth-library": ["google-auth-library@10.6.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "7.1.3", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA=="],
|
||||
|
||||
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
|
||||
|
||||
"googleapis": ["googleapis@171.4.0", "", { "dependencies": { "google-auth-library": "^10.2.0", "googleapis-common": "^8.0.0" } }, "sha512-xybFL2SmmUgIifgsbsRQYRdNrSAYwxWZDmkZTGjUIaRnX5jPqR8el/cEvo6rCqh7iaZx6MfEPS/lrDgZ0bymkg=="],
|
||||
|
||||
"googleapis-common": ["googleapis-common@8.0.1", "", { "dependencies": { "extend": "^3.0.2", "gaxios": "^7.0.0-rc.4", "google-auth-library": "^10.1.0", "qs": "^6.7.0", "url-template": "^2.0.8" } }, "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
@@ -1660,12 +1588,8 @@
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
||||
|
||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
|
||||
|
||||
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||
@@ -1680,10 +1604,6 @@
|
||||
|
||||
"jsonify": ["jsonify@0.0.1", "", {}, "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg=="],
|
||||
|
||||
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
||||
|
||||
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
|
||||
@@ -1866,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=="],
|
||||
@@ -1896,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=="],
|
||||
@@ -2022,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=="],
|
||||
|
||||
@@ -2124,8 +2036,6 @@
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
|
||||
|
||||
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
@@ -2136,8 +2046,6 @@
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
|
||||
@@ -2192,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=="],
|
||||
@@ -2238,8 +2144,6 @@
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"swr": ["swr@2.4.1", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA=="],
|
||||
|
||||
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
|
||||
|
||||
"synckit": ["synckit@0.11.12", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="],
|
||||
@@ -2256,8 +2160,6 @@
|
||||
|
||||
"test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="],
|
||||
|
||||
"throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="],
|
||||
|
||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||
|
||||
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||
@@ -2338,10 +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=="],
|
||||
|
||||
"url-template": ["url-template@2.0.8", "", {}, "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw=="],
|
||||
|
||||
"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=="],
|
||||
@@ -2544,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=="],
|
||||
@@ -2566,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=="],
|
||||
@@ -2586,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=="],
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
"test": "vitest --typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^3.0.41",
|
||||
"@ai-sdk/react": "^3.0.118",
|
||||
"@clerk/nextjs": "^7.0.2",
|
||||
"@electric-sql/pglite": "^0.3.16",
|
||||
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||
@@ -65,8 +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",
|
||||
"ai": "^6.0.116",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -76,7 +72,6 @@
|
||||
"drizzle-zod": "^0.8.3",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"glazejs": "^2.0.1",
|
||||
"googleapis": "^171.4.0",
|
||||
"gsap": "^3.14.2",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.577.0",
|
||||
@@ -99,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,32 +0,0 @@
|
||||
'use client'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui/dialog'
|
||||
import ChatInterface from '~/app/chat/_components/ChatInterface'
|
||||
|
||||
type DBMessage = {
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
}
|
||||
|
||||
interface ChatModalProps {
|
||||
sessionId?: string
|
||||
// initialMessages: DBMessage[]
|
||||
}
|
||||
|
||||
export default function ChatModal({ sessionId }: ChatModalProps) {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<Dialog modal={true} open onOpenChange={() => router.back()}>
|
||||
<DialogContent className="w-full max-w-full rounded-none sm:max-w-full h-[100svh] lg:max-w-3xl lg:rounded-xl lg:h-[80vh] flex flex-col p-0 gap-0">
|
||||
<DialogHeader className="p-4 border-b shrink-0">
|
||||
<DialogTitle>Talk To My AI-Assistant</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-hidden min-h-0">
|
||||
<ChatInterface sessionId={sessionId} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
'use client'
|
||||
import { Skeleton } from '~/components/ui/skeleton';
|
||||
import ChatModal from './_components/ChatModal'
|
||||
import { trpc } from '~/app/_trpc/Client'
|
||||
import { useTimeLine } from '~/app/_providers/GsapProvicer';
|
||||
|
||||
export default function AssistantModalPage() {
|
||||
const { data: session, error, isLoading } = trpc.chat.getSession.useQuery();
|
||||
return (
|
||||
<>
|
||||
&& <ChatModal sessionId={session?.id} />
|
||||
{error && <div>{error.message}</div>}
|
||||
{isLoading && <Skeleton />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { useGSAP } from "@gsap/react";
|
||||
import { useRef, type HTMLAttributes, type ReactNode } from "react";
|
||||
import { useGsapContext } from "~/app/_providers/GsapProvicer";
|
||||
import { SplitText } from "gsap/SplitText";
|
||||
import gsap from 'gsap'
|
||||
import { cn } from "~/lib/utils";
|
||||
const AnimateTextIn = ({
|
||||
children,
|
||||
animation = "type",
|
||||
position = 0,
|
||||
tlId = undefined,
|
||||
speed = 1,
|
||||
scrollOnly = false,
|
||||
className
|
||||
}: {
|
||||
children: ReactNode,
|
||||
animation?: "type" | "slide",
|
||||
position?: gsap.Position,
|
||||
tlId?: string,
|
||||
scrollOnly?: boolean,
|
||||
speed?: number,
|
||||
className?: HTMLAttributes<HTMLDivElement>['className']
|
||||
}) => {
|
||||
const el = useRef<HTMLDivElement>(null)
|
||||
const gsapContext = useGsapContext();
|
||||
useGSAP(() => {
|
||||
const rect = el.current?.getBoundingClientRect()
|
||||
const scroller = gsapContext?.getScroller()
|
||||
console.log(scroller)
|
||||
let viewportTop = 0
|
||||
let viewportBottom = window.innerHeight
|
||||
if (scroller && scroller instanceof Element) {
|
||||
const scrollerRect = scroller.getBoundingClientRect()
|
||||
viewportTop = scrollerRect.top
|
||||
viewportBottom = scrollerRect.top + scrollerRect.height
|
||||
}
|
||||
const isInView = rect && rect.bottom > viewportTop && rect.top < viewportBottom
|
||||
console.log(isInView)
|
||||
const chars = new SplitText(el.current, { type: 'chars' })
|
||||
gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100, duration: 0 }), 0, tlId)
|
||||
const fromVars = animation === "slide"
|
||||
? { opacity: 0, x: -10, duration: 0.2 * speed, stagger: { each: 0.08 * speed }, ease: 'bounce.inOut', onComplete: () => chars.revert() }
|
||||
: { opacity: 0, duration: 0.01 * speed, stagger: { each: 0.04 * speed }, ease: 'bounce.inOut', onComplete: () => chars.revert() }
|
||||
if (isInView && !scrollOnly) {
|
||||
gsapContext?.addAnimation(gsap.from(chars.chars, fromVars), position, tlId)
|
||||
} else {
|
||||
gsap.from(chars.chars,
|
||||
{
|
||||
...fromVars,
|
||||
scrollTrigger: {
|
||||
trigger: el.current,
|
||||
start: 'top bottom',
|
||||
end: 'bottom top',
|
||||
toggleActions: "play reverse play reverse",
|
||||
scroller
|
||||
}
|
||||
})
|
||||
}
|
||||
}, { dependencies: [] })
|
||||
return (
|
||||
<div ref={el} className={cn(className, "opacity-0")}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AnimateTextIn;
|
||||
@@ -1,22 +0,0 @@
|
||||
import { type HTMLAttributes, type ReactNode } from "react";
|
||||
import AnimatedDiv from "./AnimatedDiv";
|
||||
import { cn } from "~/lib/utils";
|
||||
const AnimatePopUp = ({
|
||||
children,
|
||||
position,
|
||||
className,
|
||||
duration=1,
|
||||
ease='elastic'
|
||||
}:{
|
||||
children:ReactNode
|
||||
position:gsap.Position,
|
||||
className?:HTMLAttributes<HTMLDivElement>['className']
|
||||
duration?:number,
|
||||
ease?:gsap.EaseString|gsap.EaseFunction
|
||||
}) => {
|
||||
return (
|
||||
<AnimatedDiv children={children} position={position} className={cn(className,'h-0 translate-y-[50] overflow-hidden')} height='auto' y={0} overflow='' ease={ease} duration={duration} />
|
||||
)
|
||||
}
|
||||
|
||||
export default AnimatePopUp;
|
||||
@@ -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,41 +0,0 @@
|
||||
import gsap from "gsap";
|
||||
import { type HTMLAttributes,
|
||||
type ReactNode, useLayoutEffect, useRef } from "react";
|
||||
import { useGsapContext } from "~/app/_providers/GsapProvicer";
|
||||
const AnimatedDiv = (
|
||||
{
|
||||
children,
|
||||
position,
|
||||
className,
|
||||
animationMode='to',
|
||||
...tweenVars
|
||||
}:
|
||||
gsap.TweenVars & {
|
||||
children:ReactNode,
|
||||
position:gsap.Position,
|
||||
animationMode?: 'from'|'to',
|
||||
className?:HTMLAttributes<HTMLDivElement>['className']
|
||||
}
|
||||
) => {
|
||||
const div = useRef<HTMLDivElement>(null);
|
||||
const gsapContext = useGsapContext()
|
||||
useLayoutEffect(() => {
|
||||
let tween:gsap.core.Tween;
|
||||
switch(animationMode) {
|
||||
case 'from':
|
||||
tween = gsap.from(div.current,tweenVars);
|
||||
break;
|
||||
case 'to':
|
||||
tween = gsap.to(div.current,tweenVars);
|
||||
break;
|
||||
}
|
||||
gsapContext?.addAnimation(tween,position)
|
||||
},[])
|
||||
return (
|
||||
<div ref={div} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AnimatedDiv;
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useGSAP } from "@gsap/react"; import { useEffect, useLayoutEffect, useRef,type ReactNode } from "react";
|
||||
import { useGsapContext } from "~/app/_providers/GsapProvicer";
|
||||
import { SplitText } from "gsap/SplitText";
|
||||
import gsap from 'gsap'
|
||||
const AnimatedPageTitle = (
|
||||
{ children, position }: { children: ReactNode, position:gsap.Position }
|
||||
) => {
|
||||
const el = useRef<HTMLHeadingElement>(null)
|
||||
const gsapContext = useGsapContext();
|
||||
useLayoutEffect(() => {
|
||||
const split = new SplitText(el.current, { type: "lines,chars", autoSplit:true })
|
||||
gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100 }),position)
|
||||
gsapContext?.addAnimation(gsap.from(split.chars, { id: 'titlesplit',
|
||||
stagger: 0.05, rotate: -90, opacity: 0, x: -10, onComplete: () => {split.revert()}
|
||||
}),'>')
|
||||
},[])
|
||||
return (
|
||||
<h1 className="text-4xl break-keep opacity-0 font-bold text-balance w-full" ref={el}> {children} </h1>
|
||||
)
|
||||
}
|
||||
|
||||
export default AnimatedPageTitle;
|
||||
@@ -1,25 +0,0 @@
|
||||
'use client'
|
||||
import Link from 'next/link'
|
||||
import { MessageCircle } from 'lucide-react'
|
||||
import { Show } from '@clerk/nextjs'
|
||||
import { Button } from '~/components/ui/button'
|
||||
import { usePathname } from 'next/navigation'
|
||||
export default function ChatFAB() {
|
||||
const pathName = usePathname()
|
||||
const isChat = pathName.indexOf('\/chat') > -1
|
||||
return (
|
||||
<>
|
||||
{!isChat &&
|
||||
<Show when="signed-in">
|
||||
<div className="fixed bottom-6 right-6 z-50">
|
||||
<Button asChild size="icon" className="h-14 w-14 rounded-full shadow-lg">
|
||||
<Link href="/assistant">
|
||||
<MessageCircle className="h-6 w-6" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ThemeSwitch } from "./ThemeSwitch"
|
||||
|
||||
export default function TopNav() {
|
||||
return (
|
||||
<div className="fixed backdrop-blur-md lg:w-full right-0 z-50">
|
||||
<div className="fixed lg:w-full right-0 z-50 lg:bg-background">
|
||||
<nav className="flex flex-col-reverse lg:flex-row flex-wrap w-20 lg:w-full outline-1 lg:h-10 h-full">
|
||||
<div className="flex flex-wrap lg:h-full w-20 lg:w-fit lg:flex-row">
|
||||
<Button className="flex h-10 lg:h-full w-full lg:w-20" asChild variant="outline">
|
||||
@@ -19,14 +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>
|
||||
<Show when="signed-in">
|
||||
<Button asChild className="flex h-10 lg:h-full w-full lg:w-20" variant="outline">
|
||||
<Link href="/chat"> Chat </Link>
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
<div className="flex flex-col-reverse flex-wrap lg:h-full w-20 lg:w-fit lg:flex-row lg:ml-auto">
|
||||
<AdminWrap>
|
||||
@@ -52,13 +44,7 @@ export default function TopNav() {
|
||||
<Show when="signed-in">
|
||||
<Button asChild className="flex h-10 lg:h-full cursor-pointer lg:w-20 content-center" variant={"outline"}>
|
||||
<div>
|
||||
<UserButton
|
||||
userProfileProps={{
|
||||
additionalOAuthScopes: {
|
||||
google: ['https://www.googleapis.com/auth/calendar'],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<UserButton />
|
||||
</div>
|
||||
</Button>
|
||||
</Show>
|
||||
|
||||
@@ -1,98 +1,18 @@
|
||||
'use client'
|
||||
import { useGSAP } from '@gsap/react'
|
||||
import gsap from 'gsap'
|
||||
import { SplitText } from 'gsap/SplitText'
|
||||
import { ScrollTrigger, GSDevTools } from 'gsap/all'
|
||||
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useRef, type ReactNode } from 'react'
|
||||
import { createContext, useContext, type ReactNode } from 'react'
|
||||
|
||||
gsap.registerPlugin(useGSAP)
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
gsap.registerPlugin(SplitText)
|
||||
gsap.registerPlugin(GSDevTools)
|
||||
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])
|
||||
useLayoutEffect(() => {
|
||||
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))) {
|
||||
let scrollers = document.querySelectorAll('[data-slot="scroll-area-viewport"]')
|
||||
if (scrollers.length < 1) {
|
||||
scrollerRef.current = window
|
||||
} else {
|
||||
let scrollerArray = Array.from(scrollers.values()).sort((a,b) => {
|
||||
const s1 = a as HTMLDivElement;
|
||||
const s2 = b as HTMLDivElement;
|
||||
// using bitwise not (~~) to coerce NaN values to 0
|
||||
const aPriority = ~~Number(s1.dataset?.scrollerPriority)
|
||||
const bPriority = ~~Number(s2.dataset?.scrollerPriority)
|
||||
return aPriority - bPriority;
|
||||
})
|
||||
let prioScroller = scrollerArray.pop();
|
||||
scrollerRef.current = prioScroller || 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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,5 @@ import { env } from "~/env"
|
||||
|
||||
export async function isAdmin() {
|
||||
const userid = (await auth()).userId
|
||||
console.log(userid)
|
||||
return (userid == env.ADMIN_USER_CLERK_ID)
|
||||
}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
'use server'
|
||||
import { clerkClient } from '@clerk/nextjs/server'
|
||||
import { google } from 'googleapis'
|
||||
import { env } from '~/env'
|
||||
|
||||
export async function scheduleMeeting({
|
||||
title,
|
||||
description,
|
||||
dateTime,
|
||||
durationMinutes,
|
||||
attendeeEmail,
|
||||
attendeeName,
|
||||
userId,
|
||||
}: {
|
||||
title: string
|
||||
description: string
|
||||
dateTime: string
|
||||
durationMinutes: number
|
||||
attendeeEmail?: string
|
||||
attendeeName?: string
|
||||
userId: string
|
||||
}) {
|
||||
try {
|
||||
const clerk = await clerkClient()
|
||||
|
||||
// Get admin's Google OAuth token to create the event on Gregor's calendar
|
||||
const adminTokenResponse = await clerk.users.getUserOauthAccessToken(
|
||||
env.ADMIN_USER_CLERK_ID,
|
||||
'oauth_google',
|
||||
)
|
||||
const adminToken = adminTokenResponse.data[0]
|
||||
|
||||
if (!adminToken?.token) {
|
||||
return { success: false, error: 'Admin Google Calendar not connected. Ensure the admin account is linked with Google and has calendar scope enabled.' }
|
||||
}
|
||||
|
||||
// Try to resolve visitor's Google email for the invite
|
||||
let visitorEmail: string | undefined = attendeeEmail
|
||||
if (!visitorEmail) {
|
||||
try {
|
||||
const visitorTokenResponse = await clerk.users.getUserOauthAccessToken(userId, 'oauth_google')
|
||||
if (visitorTokenResponse.data[0]) {
|
||||
const user = await clerk.users.getUser(userId)
|
||||
const googleAccount = user.externalAccounts.find((a) => a.provider === 'google')
|
||||
visitorEmail = googleAccount?.emailAddress ?? undefined
|
||||
}
|
||||
} catch {
|
||||
// Visitor not signed in with Google — no invite
|
||||
}
|
||||
}
|
||||
|
||||
const oAuth2Client = new google.auth.OAuth2()
|
||||
oAuth2Client.setCredentials({ access_token: adminToken.token })
|
||||
const calendar = google.calendar({ version: 'v3', auth: oAuth2Client })
|
||||
|
||||
const startTime = new Date(dateTime)
|
||||
const endTime = new Date(startTime.getTime() + durationMinutes * 60 * 1000)
|
||||
|
||||
const attendees: { email: string; displayName?: string }[] = []
|
||||
if (visitorEmail) {
|
||||
attendees.push({ email: visitorEmail, displayName: attendeeName })
|
||||
}
|
||||
|
||||
const event = await calendar.events.insert({
|
||||
calendarId: 'primary',
|
||||
sendUpdates: 'all',
|
||||
requestBody: {
|
||||
summary: title,
|
||||
description,
|
||||
start: { dateTime: startTime.toISOString(), timeZone: 'UTC' },
|
||||
end: { dateTime: endTime.toISOString(), timeZone: 'UTC' },
|
||||
attendees,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
eventId: event.data.id,
|
||||
htmlLink: event.data.htmlLink,
|
||||
message: `Meeting "${title}" scheduled for ${startTime.toLocaleString()}${visitorEmail ? `. Invite sent to ${visitorEmail}.` : '.'}`,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to schedule meeting:', error)
|
||||
return { success: false, error: 'Failed to schedule meeting. Please try again.' }
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import Link from "next/link";
|
||||
import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger } from "~/components/ui/sidebar";
|
||||
import SimpleSidebarGroup from "~/components/ui/simple-sidebar-group";
|
||||
|
||||
export default function AdminSideBar() {
|
||||
export default async function AdminSideBar() {
|
||||
return (
|
||||
<>
|
||||
<SidebarProvider>
|
||||
@@ -20,15 +20,9 @@ export default 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>
|
||||
<SimpleSidebarGroup lable="Chat">
|
||||
<Link href={"/admin/chat"}> System Prompt </Link>
|
||||
</SimpleSidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { Textarea } from '~/components/ui/textarea'
|
||||
import { Button } from '~/components/ui/button'
|
||||
import { trpc } from '~/app/_trpc/Client'
|
||||
|
||||
export default function SystemPromptForm({ initialValue }: { initialValue: string }) {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [saved, setSaved] = useState(false)
|
||||
|
||||
const mutation = trpc.chat.updateSystemPrompt.useMutation({
|
||||
onSuccess: () => setSaved(true),
|
||||
})
|
||||
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
setSaved(false)
|
||||
mutation.mutate({ prompt: value })
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4 w-full">
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={(e) => { setValue(e.target.value); setSaved(false) }}
|
||||
rows={16}
|
||||
className="font-mono text-sm resize-y"
|
||||
placeholder="Enter the system prompt for the AI recruiter..."
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
{mutation.isPending ? 'Saving…' : 'Save'}
|
||||
</Button>
|
||||
{saved && <span className="text-sm text-muted-foreground">Saved</span>}
|
||||
{mutation.error && <span className="text-sm text-destructive">{mutation.error.message}</span>}
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { isAdmin } from '~/app/actions'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { servTrpc } from '~/app/_trpc/ServerClient'
|
||||
import SystemPromptForm from './_components/SystemPromptForm'
|
||||
|
||||
export default async function SystemPromptPage() {
|
||||
if (!(await isAdmin())) redirect('/admin')
|
||||
|
||||
const prompt = await servTrpc.chat.getSystemPrompt()
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-2xl p-6 flex flex-col gap-4">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold">AI System Prompt</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This prompt is sent to the model on every chat request.
|
||||
</p>
|
||||
</div>
|
||||
<SystemPromptForm initialValue={prompt} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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,8 +1,8 @@
|
||||
'use server'
|
||||
|
||||
import AdminSideBar from "./_components/AdminSideBar";
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default function Admin({children}: Readonly<{children: React.ReactNode}>) {
|
||||
export default async function Admin({children}: Readonly<{children: React.ReactNode}>) {
|
||||
return (
|
||||
<>
|
||||
<AdminSideBar/>
|
||||
|
||||
@@ -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,98 +0,0 @@
|
||||
import { auth } from '@clerk/nextjs/server'
|
||||
import { createOpenAI } from '@ai-sdk/openai'
|
||||
import { streamText, tool, convertToModelMessages, stepCountIs, type UIMessage } from 'ai'
|
||||
import { success, z } from 'zod'
|
||||
import { eq, and } from 'drizzle-orm'
|
||||
import { env } from '~/env'
|
||||
import { db } from '~/server/db'
|
||||
import { chatSession, chatMessage } from '~/server/dbschema/schema'
|
||||
import { servTrpc } from '~/app/_trpc/ServerClient'
|
||||
import { scheduleMeeting } from '~/app/actions/scheduleMeeting'
|
||||
|
||||
const openai = createOpenAI({ apiKey: env.OPENAI_API_KEY })
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { userId } = await auth()
|
||||
if (userId == null) return new Response('Unauthorized', { status: 401 })
|
||||
|
||||
const { messages, sessionId } = (await req.json()) as {
|
||||
messages: UIMessage[]
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
// Verify this session belongs to the authenticated user
|
||||
const session = await db
|
||||
.select()
|
||||
.from(chatSession)
|
||||
.where(and(eq(chatSession.id, sessionId), eq(chatSession.userId, userId)))
|
||||
.limit(1)
|
||||
.then((r) => r[0])
|
||||
|
||||
if (!session) return new Response('Session not found', { status: 404 })
|
||||
|
||||
const systemPrompt = await servTrpc.chat.getSystemPrompt() || 'You are an AI recruiter assistant.'
|
||||
|
||||
// Save the latest user message
|
||||
const lastMessage = messages[messages.length - 1]
|
||||
if (lastMessage?.role === 'user') {
|
||||
const content = lastMessage.parts
|
||||
.filter((p): p is { type: 'text'; text: string } => p.type === 'text')
|
||||
.map((p) => p.text)
|
||||
.join('')
|
||||
if (content) {
|
||||
await db.insert(chatMessage).values({ sessionId, role: 'user', content })
|
||||
}
|
||||
}
|
||||
|
||||
const result = streamText({
|
||||
model: openai('gpt-4o'),
|
||||
system: systemPrompt,
|
||||
messages: await convertToModelMessages(messages),
|
||||
tools: {
|
||||
scheduleMeeting: tool({
|
||||
description: 'Schedule a meeting with Gregor Lohaus and add it to his Google Calendar',
|
||||
inputSchema: z.object({
|
||||
title: z.string().describe('Meeting title'),
|
||||
description: z.string().describe('Meeting description / agenda'),
|
||||
dateTime: z
|
||||
.string()
|
||||
.describe(
|
||||
'ISO 8601 datetime for the meeting start, e.g. 2025-04-01T10:00:00',
|
||||
),
|
||||
durationMinutes: z
|
||||
.number()
|
||||
.int()
|
||||
.min(15)
|
||||
.max(120)
|
||||
.describe('Duration of the meeting in minutes'),
|
||||
attendeeEmail: z
|
||||
.string()
|
||||
.email()
|
||||
.optional()
|
||||
.describe('Email of the visitor to invite (if provided)'),
|
||||
attendeeName: z.string().optional().describe('Name of the visitor'),
|
||||
}),
|
||||
execute: async (input) => scheduleMeeting({ ...input, userId }),
|
||||
}),
|
||||
getCurrentUnixTime: tool({
|
||||
description: 'Get the current unix time to reference for meeting dates',
|
||||
inputSchema: z.object({
|
||||
none: z.string().optional().describe("no inputs are needed")
|
||||
}),
|
||||
execute: async () => { return {success: true, currentTime: Date.now()} }
|
||||
})
|
||||
},
|
||||
stopWhen: stepCountIs(5),
|
||||
onFinish: async ({ text, finishReason }) => {
|
||||
if (text && finishReason === 'stop') {
|
||||
await db.insert(chatMessage).values({
|
||||
sessionId,
|
||||
role: 'assistant',
|
||||
content: text,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return result.toUIMessageStreamResponse()
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { createRouteHandler } from "uploadthing/next";
|
||||
import { fileRouter } from "~/server/uploadthing";
|
||||
|
||||
export const { GET, POST } = createRouteHandler({
|
||||
router: fileRouter,
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default function AssistantPage() {
|
||||
redirect('/chat')
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import type { UIMessage } from "ai";
|
||||
import Markdown from "react-markdown";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
export const AssistantMessage = (props: { message: UIMessage }) => {
|
||||
let message = props.message;
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
className='flex justify-start'
|
||||
>
|
||||
<div
|
||||
className=
|
||||
'max-w-[80%] px-4 py-2 text-sm space-y-2 bg-muted'
|
||||
>
|
||||
{message.parts.map((part, i) => {
|
||||
if (part.type === 'text') {
|
||||
return (
|
||||
<Markdown key={i}>
|
||||
{part.text}
|
||||
</Markdown>
|
||||
)
|
||||
}
|
||||
if (part.type === 'tool-scheduleMeeting') {
|
||||
const toolPart = part as unknown as {
|
||||
type: 'tool-scheduleMeeting'
|
||||
state: string
|
||||
input: unknown
|
||||
output?: { success: boolean; message?: string; htmlLink?: string; error?: string }
|
||||
}
|
||||
if (toolPart.state === 'input-available' || toolPart.state === 'input-streaming') {
|
||||
return (
|
||||
<p key={i} className="text-xs opacity-70 italic">
|
||||
Scheduling meeting…
|
||||
</p>
|
||||
)
|
||||
}
|
||||
if (toolPart.state === 'output-available' && toolPart.output) {
|
||||
const result = toolPart.output
|
||||
return (
|
||||
<div key={i} className="text-xs mt-1 p-2 bg-background/20 rounded">
|
||||
{result.success ? (
|
||||
<span>
|
||||
✓ {result.message}{' '}
|
||||
{result.htmlLink && (
|
||||
<a
|
||||
href={result.htmlLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
View event
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span>✗ {result.error}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
'use client'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useChat } from '@ai-sdk/react'
|
||||
import { DefaultChatTransport, type UIMessage } from 'ai'
|
||||
import { Button } from '~/components/ui/button'
|
||||
import { Textarea } from '~/components/ui/textarea'
|
||||
import {
|
||||
useGsapContext,
|
||||
} from '~/app/_providers/GsapProvicer';
|
||||
import Messages from './Messages'
|
||||
import { DeleteIcon } from 'lucide-react';
|
||||
import { trpc } from '~/app/_trpc/Client'
|
||||
import { Spinner } from '~/components/ui/spinner';
|
||||
interface DBMessage {
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
}
|
||||
|
||||
interface ChatInterfaceProps {
|
||||
sessionId?: string
|
||||
// initialMessages: DBMessage[]
|
||||
}
|
||||
|
||||
function toUIMessages(dbMessages: DBMessage[]): UIMessage[] {
|
||||
return dbMessages.map((m) => ({
|
||||
id: m.id,
|
||||
role: m.role,
|
||||
parts: [{ type: 'text' as const, text: m.content }],
|
||||
}))
|
||||
}
|
||||
export default function ChatInterface({ sessionId }: ChatInterfaceProps) {
|
||||
const utils = trpc.useUtils();
|
||||
const { data: dbMessages, refetch: refetchMessages } = trpc.chat.getMessages.useQuery(sessionId ? sessionId : "")
|
||||
const [messages, setMessages] = useState<UIMessage[]>([]);
|
||||
function addMessage(newMessage: UIMessage) {
|
||||
setMessages(prev => [...prev, newMessage]);
|
||||
}
|
||||
useEffect(() => {
|
||||
setMessages(toUIMessages(dbMessages ?? []));
|
||||
}, [dbMessages]);
|
||||
|
||||
if (messages.at(0)?.id != 'init') {
|
||||
messages.unshift({
|
||||
id: "init",
|
||||
role: 'assistant',
|
||||
parts: [{
|
||||
type: 'text',
|
||||
text: "Hi im gregors ai assistant,you can ask me to provide general information or to schedule a meeting."
|
||||
}],
|
||||
})
|
||||
}
|
||||
const [input, setInput] = useState('')
|
||||
const { sendMessage, status, error, clearError } = useChat({
|
||||
transport: new DefaultChatTransport({
|
||||
api: '/api/chat', body: { sessionId },
|
||||
}),
|
||||
messages: messages,
|
||||
})
|
||||
const handleSend = () => {
|
||||
const text = input.trim()
|
||||
if (!text || status != 'ready') return
|
||||
setInput('')
|
||||
sendMessage({ text })
|
||||
addMessage({
|
||||
id: "", role: "user", parts: [
|
||||
{ type: 'text', text }
|
||||
]
|
||||
})
|
||||
addMessage({
|
||||
id: "", role: "assistant", parts: [
|
||||
{ type: 'text', text: "Thinking..." }
|
||||
]
|
||||
})
|
||||
}
|
||||
const clearChatMutation = trpc.chat.clearChat.useMutation()
|
||||
const handleClear = () => {
|
||||
clearChatMutation.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
utils.chat.getMessages.invalidate()
|
||||
refetchMessages()
|
||||
}
|
||||
})
|
||||
}
|
||||
const gsapContext = useGsapContext()
|
||||
useEffect(() => {
|
||||
console.log(status)
|
||||
if (status == 'ready') {
|
||||
utils.chat.getMessages.invalidate();
|
||||
refetchMessages()
|
||||
}
|
||||
}, [status])
|
||||
useEffect(() => {
|
||||
let scroller = gsapContext?.getScroller()
|
||||
if (scroller instanceof Window) {
|
||||
return;
|
||||
}
|
||||
console.log(scroller?.scrollHeight)
|
||||
scroller?.scrollTo({ behavior: 'smooth', top: scroller.scrollHeight })
|
||||
}, [messages])
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{messages &&
|
||||
<Messages messages={messages} />
|
||||
}
|
||||
|
||||
{error && (
|
||||
<div className="mx-4 mb-2 flex items-start gap-2 rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
<span className="flex-1">
|
||||
{error.message.includes('quota') || error.message.includes('429')
|
||||
? 'OpenAI quota exceeded. Please try again later.'
|
||||
: `Error: ${error.message}`}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={clearError}
|
||||
className="shrink-0 opacity-60 hover:opacity-100"
|
||||
variant='destructive'
|
||||
>
|
||||
<DeleteIcon />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4 border-t flex flex-row gap-2">
|
||||
<Textarea
|
||||
name='message'
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="Ask about Gregor's experience or schedule a meeting…"
|
||||
className="resize-none"
|
||||
rows={2}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
disabled={status != "ready" || !input.trim()}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
<Button
|
||||
variant='destructive'
|
||||
onClick={handleClear}
|
||||
disabled={status != "ready" || clearChatMutation.isPending}
|
||||
>
|
||||
{clearChatMutation.isPending ?
|
||||
<Spinner /> :
|
||||
"Clear Chat"
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { type UIMessage } from 'ai'
|
||||
import * as Card from "~/components/ui/card"
|
||||
import AnimateTextIn from '~/app/_components/Animated/AnimateIn';
|
||||
import { UserMessage } from './UserMessage';
|
||||
import { AssistantMessage } from './AssistantMessage';
|
||||
import { ScrollArea } from '~/components/ui/scroll-area';
|
||||
import { useTimeLine } from '~/app/_providers/GsapProvicer';
|
||||
import {
|
||||
memo
|
||||
} from 'react';
|
||||
const Messages = memo(({ messages}: { messages: UIMessage[]}) => {
|
||||
return (
|
||||
<ScrollArea data-scroller-priority='1' className="w-full h-[90%] max-w-4xl mx-auto">
|
||||
{messages.map((message, i) => (
|
||||
<Card.AnimatedCard scrollOnly={true} tlId='chat' position={i * 0.2} key={i}>
|
||||
<Card.CardContent>
|
||||
{message.role == 'assistant' && <AssistantMessage message={message} />}
|
||||
{message.role == 'user' && <UserMessage message={message} />}
|
||||
</Card.CardContent>
|
||||
</Card.AnimatedCard>
|
||||
))}
|
||||
</ScrollArea>)
|
||||
})
|
||||
|
||||
export default Messages;
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { UIMessage } from "ai"
|
||||
|
||||
export const UserMessage = (props:{message: UIMessage}) => {
|
||||
let message = props.message.parts.reduce((acc, part) => {
|
||||
if (part.type == 'text') {
|
||||
return acc + part.text
|
||||
}
|
||||
return acc
|
||||
},"");
|
||||
return (
|
||||
<div
|
||||
key={props.message.id}
|
||||
className='flex justify-end'
|
||||
>
|
||||
<div
|
||||
className=
|
||||
'max-w-[80%] px-4 py-2 text-sm space-y-2 bg-primary'
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
'use client'
|
||||
import ChatInterface from './_components/ChatInterface'
|
||||
import { trpc } from '../_trpc/Client';
|
||||
import { Skeleton } from '~/components/ui/skeleton';
|
||||
import AnimatedPageTitle from '../_components/Animated/AnimatedPageTitle';
|
||||
import { useTimeLine } from '../_providers/GsapProvicer';
|
||||
|
||||
export default function ChatPage() {
|
||||
const { data: session, error, isLoading } = trpc.chat.getSession.useQuery();
|
||||
useTimeLine(session)
|
||||
return (
|
||||
<div className="flex flex-col px-10 lg:px-0 w-full h-full max-w-4xl mx-auto pt-10">
|
||||
<AnimatedPageTitle position={0}>
|
||||
<span>Talk To My </span> <span> AI-Assistant</span>
|
||||
</AnimatedPageTitle>
|
||||
<div className='flex items-center h-[80%] my-auto w-full'>
|
||||
<ChatInterface sessionId={session?.id} />
|
||||
{error && <div>{error.message}</div>}
|
||||
{isLoading && <Skeleton/>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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]">
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ClerkProvider } from "@clerk/nextjs";
|
||||
import { config } from "@fortawesome/fontawesome-svg-core"
|
||||
import "@fortawesome/fontawesome-svg-core/styles.css"
|
||||
import TopNav from "./_components/TopNav";
|
||||
import ChatFAB from "./_components/ChatFAB";
|
||||
import TrpcProvider from "./_trpc/TrpcProvider";
|
||||
// import dynamic from "next/dynamic";
|
||||
// const ThemeProvider = dynamic(() => import("./_providers/ThemeProvider"),{ssr:true})
|
||||
@@ -13,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' });
|
||||
|
||||
@@ -30,6 +28,7 @@ const geist = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
});
|
||||
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
modal
|
||||
@@ -45,14 +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 lg:h-[calc(100vh-var(--spacing)*10)] w-screen">
|
||||
<main className="absolute lg:top-10 h-screen w-screen">
|
||||
{children}
|
||||
</main>
|
||||
{modal}
|
||||
</AnimatedBackGroundContainer>
|
||||
<ChatFAB />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,63 +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";
|
||||
import AnimatePopUp from "../_components/Animated/AnimatePopUp";
|
||||
export default function MusicPage() {
|
||||
const { data: tracks, isLoading } = trpc.music.list.useQuery();
|
||||
useTimeLine(tracks)
|
||||
return (
|
||||
<ScrollArea className="px-10 lg:px-0 w-full h-full max-w-4xl mx-auto pt-10">
|
||||
<AnimatedPageTitle position={0}><span>Just Some </span> <span>Music I Made</span> </AnimatedPageTitle>
|
||||
<div className="flex flex-wrap h-fit content-center">
|
||||
<AnimateTextIn className="flex flex-wrap mr-[1em]" position={0.5}>
|
||||
<div><p className="break-after-avoid mr-[1em]">All works on this page are licensed under:</p></div>
|
||||
<div><a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a></div>
|
||||
</AnimateTextIn>
|
||||
<AnimatePopUp position={2} className="items-center content-center">
|
||||
<div className="flex flex-row">
|
||||
<img className="max-w-[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>
|
||||
</AnimatePopUp>
|
||||
</div>
|
||||
<div className="pt-10" />
|
||||
{tracks && tracks.map((track, i) => (
|
||||
<div key={track.id}>
|
||||
<Card.AnimatedCard 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>
|
||||
)}
|
||||
<AnimatePopUp position={i + 1.3}>
|
||||
<audio controls className="w-full player" src={track.fileUrl}>
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</AnimatePopUp>
|
||||
</Card.CardContent>
|
||||
</Card.AnimatedCard>
|
||||
<div className="pt-5" />
|
||||
</div>
|
||||
))}
|
||||
{!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>
|
||||
);
|
||||
}
|
||||
@@ -5,16 +5,10 @@ import * as Card from "~/components/ui/card";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { StackBadge } from "~/components/StackBadge";
|
||||
import Markdown from "react-markdown";
|
||||
import { ScrollArea } from "~/components/ui/scroll-area";
|
||||
import AnimatedPageTitle from "../_components/Animated/AnimatedPageTitle";
|
||||
import AnimateTextIn from "../_components/Animated/AnimateIn";
|
||||
import { useTimeLine } from "../_providers/GsapProvicer";
|
||||
import AnimatePopUp from "../_components/Animated/AnimatePopUp";
|
||||
import { Button } from "~/components/ui/button";
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const { data: projects, isLoading } = trpc.projectv2.listWithStack.useQuery();
|
||||
useTimeLine(projects)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[200px] text-muted-foreground">
|
||||
@@ -32,83 +26,68 @@ export default function ProjectsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea className="px-10 lg:px-0 w-full h-full max-w-4xl mx-auto pt-10">
|
||||
<AnimatedPageTitle position={0}><span>Project I've Been</span><span> Working on</span> </AnimatedPageTitle>
|
||||
<div className="pt-10" />
|
||||
{projects.map((project, i) => (
|
||||
<div key={i}>
|
||||
<Card.AnimatedCard position={i + 1.2} key={project.id}>
|
||||
<Card.CardHeader>
|
||||
<div className="flex items-start justify-between gap-2 flex-wrap">
|
||||
<AnimateTextIn position={i + 1.4} animation="slide"><Card.CardTitle>{project.title}</Card.CardTitle></AnimateTextIn>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{project.sourceType && (
|
||||
<AnimatePopUp position={i + 2} duration={2}>
|
||||
<Badge variant={project.sourceType === "open" ? "secondary" : "outline"}>
|
||||
{project.sourceType === "open" ? "Open Source" : "Closed Source"}
|
||||
</Badge>
|
||||
</AnimatePopUp>
|
||||
)}
|
||||
{project.releaseStatus && (
|
||||
<Badge variant={project.releaseStatus === "released" ? "default" : "outline"}>
|
||||
{project.releaseStatus === "released" ? "Released" : "Unreleased"}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card.CardHeader>
|
||||
{(project.description || project.sourceLink || project.releaseLink || project.techStack?.stackItems?.length) && (
|
||||
<Card.CardContent className="flex flex-col gap-3">
|
||||
{project.description && (
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none text-muted-foreground">
|
||||
<AnimatePopUp position={i + 1.4} duration={project.description.length / 20}>
|
||||
<AnimateTextIn position={i + 1.5} animation="slide"><Markdown>{project.description}</Markdown></AnimateTextIn></AnimatePopUp>
|
||||
</div>
|
||||
<div className="w-full max-w-4xl mx-auto px-4 py-8 flex flex-col gap-4">
|
||||
{projects.map((project) => (
|
||||
<Card.Card key={project.id}>
|
||||
<Card.CardHeader>
|
||||
<div className="flex items-start justify-between gap-2 flex-wrap">
|
||||
<Card.CardTitle>{project.title}</Card.CardTitle>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{project.sourceType && (
|
||||
<Badge variant={project.sourceType === "open" ? "secondary" : "outline"}>
|
||||
{project.sourceType === "open" ? "Open Source" : "Closed Source"}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="flex flex-row">
|
||||
{project.techStack?.stackItems && project.techStack.stackItems.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{project.techStack.stackItems.map((item, k) => (
|
||||
<AnimatePopUp key={k} position={(i + 2) + k * 0.5}> <StackBadge key={item} item={item} /> </AnimatePopUp>
|
||||
))}
|
||||
</div>
|
||||
{project.releaseStatus && (
|
||||
<Badge variant={project.releaseStatus === "released" ? "default" : "outline"}>
|
||||
{project.releaseStatus === "released" ? "Released" : "Unreleased"}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card.CardHeader>
|
||||
{(project.description || project.sourceLink || project.releaseLink || project.techStack?.stackItems?.length) && (
|
||||
<Card.CardContent className="flex flex-col gap-3">
|
||||
{project.description && (
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none text-muted-foreground">
|
||||
<Markdown>{project.description}</Markdown>
|
||||
</div>
|
||||
)}
|
||||
{(project.sourceLink || project.releaseLink) && (
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
{project.sourceLink && (
|
||||
<a
|
||||
href={project.sourceLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 transition-colors"
|
||||
>
|
||||
Source
|
||||
</a>
|
||||
)}
|
||||
{(project.sourceLink || project.releaseLink) && (
|
||||
<div className="ml-auto flex-col lg:flex-row justify-center gap-5">
|
||||
{project.sourceLink &&
|
||||
<Button variant='outline' className="cursor-pointer mb-3 lg:mb-0 lg:mr-3 min-w-18">
|
||||
<a
|
||||
href={project.sourceLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className='items-center'
|
||||
>
|
||||
Source
|
||||
</a>
|
||||
</Button>
|
||||
}
|
||||
{project.releaseLink &&
|
||||
<Button variant='default' className="cursor-pointer min-w-18 items-center">
|
||||
<a
|
||||
href={project.releaseLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className='items-center'
|
||||
>
|
||||
Live
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
}
|
||||
</div>
|
||||
{project.releaseLink && (
|
||||
<a
|
||||
href={project.releaseLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 transition-colors"
|
||||
>
|
||||
Live
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</Card.CardContent>
|
||||
)}
|
||||
</Card.AnimatedCard>
|
||||
<div className="pt-5" />
|
||||
</div>
|
||||
)}
|
||||
{project.techStack?.stackItems && project.techStack.stackItems.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{project.techStack.stackItems.map((item) => (
|
||||
<StackBadge key={item} item={item} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card.CardContent>
|
||||
)}
|
||||
</Card.Card>
|
||||
))}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,61 +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",
|
||||
tlId = undefined,
|
||||
scrollOnly = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm", position: gsap.Position, tlId?: string, scrollOnly?: boolean }) {
|
||||
const gsapContext = useGsapContext()
|
||||
const ref = useRef<HTMLDivElement | null>(null)
|
||||
useGSAP(() => {
|
||||
const rect = ref.current?.getBoundingClientRect()
|
||||
const scroller = gsapContext?.getScroller()
|
||||
console.log(scroller)
|
||||
let viewportTop = 0
|
||||
let viewportBottom = window.innerHeight
|
||||
if (scroller && scroller instanceof Element) {
|
||||
const scrollerRect = scroller.getBoundingClientRect()
|
||||
viewportTop = scrollerRect.top
|
||||
viewportBottom = scrollerRect.top + scrollerRect.height
|
||||
}
|
||||
const isInView = rect && rect.bottom > viewportTop && rect.top < viewportBottom
|
||||
console.log(isInView)
|
||||
const fromVars = { x: -100, opacity: 0, duration: 0.5 }
|
||||
if (isInView && !scrollOnly) {
|
||||
gsapContext?.addAnimation(gsap.from(ref.current, fromVars), position, tlId)
|
||||
} else {
|
||||
gsap.from(ref.current,
|
||||
{
|
||||
...fromVars,
|
||||
scrollTrigger: {
|
||||
trigger: ref.current,
|
||||
start: 'top bottom',
|
||||
end: 'bottom top',
|
||||
toggleActions: "play reverse play reverse",
|
||||
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}
|
||||
@@ -156,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,8 +27,6 @@ export const env = createEnv({
|
||||
|
||||
CLERK_SECRET_KEY: z.string(),
|
||||
ADMIN_USER_CLERK_ID: z.string(),
|
||||
UPLOADTHING_TOKEN: z.string(),
|
||||
OPENAI_API_KEY: z.string(),
|
||||
NODE_ENV: z
|
||||
.enum(["development", "test", "production"])
|
||||
.default("development"),
|
||||
@@ -66,8 +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,
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||
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,62 +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>(),
|
||||
})
|
||||
)
|
||||
|
||||
export const messageRoleEnum = pgEnum('message_role', ['user', 'assistant'])
|
||||
|
||||
export const chatSession = createTable(
|
||||
"chat_session",
|
||||
(d) => ({
|
||||
id: d.uuid().primaryKey().defaultRandom(),
|
||||
userId: d.varchar({ length: 255 }).notNull(),
|
||||
createdAt: d.timestamp({ withTimezone: true }).default(sql`CURRENT_TIMESTAMP`).notNull().$type<Date>(),
|
||||
updatedAt: d.timestamp({ withTimezone: true }).$onUpdate(() => new Date()).$type<Date>(),
|
||||
})
|
||||
)
|
||||
|
||||
export const chatSessionRelations = relations(chatSession, ({ many }) => ({
|
||||
messages: many(chatMessage),
|
||||
}))
|
||||
|
||||
export const chatMessage = createTable(
|
||||
"chat_message",
|
||||
(d) => ({
|
||||
id: d.uuid().primaryKey().defaultRandom(),
|
||||
sessionId: d.uuid('session_id').notNull(),
|
||||
role: messageRoleEnum().notNull(),
|
||||
content: d.text().notNull(),
|
||||
createdAt: d.timestamp({ withTimezone: true }).default(sql`CURRENT_TIMESTAMP`).notNull().$type<Date>(),
|
||||
})
|
||||
)
|
||||
|
||||
export const chatMessageRelations = relations(chatMessage, ({ one }) => ({
|
||||
session: one(chatSession, {
|
||||
fields: [chatMessage.sessionId],
|
||||
references: [chatSession.id],
|
||||
}),
|
||||
}))
|
||||
|
||||
export const systemSettings = createTable(
|
||||
"systemSetting",
|
||||
(d) => ({
|
||||
systemPropmt: d.text()
|
||||
})
|
||||
)
|
||||
|
||||
@@ -5,10 +5,8 @@ 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";
|
||||
import { chatRouter } from "./chat";
|
||||
|
||||
export const trpcRouter = router({
|
||||
project: trpcCrudRouterFromDrizzleEntity('project').router,
|
||||
@@ -19,8 +17,6 @@ export const trpcRouter = router({
|
||||
categoryv2: cvCategoryRouter,
|
||||
entry: trpcCrudRouterFromDrizzleEntity('cvEntry').router,
|
||||
entryv2: cvEntryRouter,
|
||||
music: musicRouter,
|
||||
chat: chatRouter
|
||||
});
|
||||
|
||||
export type TrpcRouter = typeof trpcRouter;
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { auth } from '@clerk/nextjs/server'
|
||||
import { publicProcedure, router } from "../trpc";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { db } from '~/server/db'
|
||||
import { chatMessage,
|
||||
chatSession, systemSettings } from "../dbschema/schema";
|
||||
import { isAdmin } from '~/app/actions';
|
||||
import { z } from 'zod';
|
||||
import { eq } from 'drizzle-orm';
|
||||
export const chatRouter = router({
|
||||
getSession: publicProcedure.query(async () => {
|
||||
const { userId } = await auth();
|
||||
if (userId == null) {
|
||||
throw new TRPCError({ message: "chat is only available to signed in users", code: 'UNAUTHORIZED' });
|
||||
}
|
||||
let session = await db.query.chatSession.findFirst({
|
||||
where(fields, operators) {
|
||||
return operators.eq(fields.userId, userId)
|
||||
},
|
||||
})
|
||||
if (session !== undefined) {
|
||||
return session;
|
||||
}
|
||||
let newSession = await db.insert(chatSession).values({ userId: userId }).returning().execute().then((r) => r.at(0));
|
||||
if (newSession == undefined) {
|
||||
throw new TRPCError({ message: "failed to create session", code: "INTERNAL_SERVER_ERROR" });
|
||||
}
|
||||
session = await db.query.chatSession.findFirst({
|
||||
where(fields, operators) {
|
||||
return operators.eq(fields.userId, userId)
|
||||
},
|
||||
})
|
||||
if (session == undefined) {
|
||||
throw new TRPCError({ message: "session not found", code: "NOT_FOUND" });
|
||||
}
|
||||
if (session !== undefined) {
|
||||
return session;
|
||||
}
|
||||
}),
|
||||
getMessages: publicProcedure.input(z.string()).query(async ({input}) => {
|
||||
let res = await db.query.chatMessage.findMany({
|
||||
where(fields,operators) {
|
||||
return operators.eq(fields.sessionId,input)
|
||||
}
|
||||
})
|
||||
return res;
|
||||
}),
|
||||
clearChat: publicProcedure.mutation(async () => {
|
||||
console.log("deleting session")
|
||||
const { userId } = await auth();
|
||||
if (userId == null) {
|
||||
throw new TRPCError({ message: "chat is only available to signed in users", code: 'UNAUTHORIZED' });
|
||||
}
|
||||
let session = await db.query.chatSession.findFirst({
|
||||
with: {
|
||||
messages: true
|
||||
},
|
||||
where(fields, operators) {
|
||||
return operators.eq(fields.userId, userId)
|
||||
},
|
||||
})
|
||||
if (session != undefined) {
|
||||
db.delete(chatMessage).where(eq(chatMessage.sessionId,session.id)).execute()
|
||||
}
|
||||
|
||||
}),
|
||||
getSystemPrompt: publicProcedure.query(async () => {
|
||||
const row = await db.select().from(systemSettings).limit(1).then((r) => r[0])
|
||||
return row?.systemPropmt ?? ''
|
||||
}),
|
||||
updateSystemPrompt: publicProcedure.input(z.object({ prompt: z.string() })).mutation(async ({ input }) => {
|
||||
if (!(await isAdmin())) throw new TRPCError({ code: 'FORBIDDEN' })
|
||||
await db.delete(systemSettings)
|
||||
await db.insert(systemSettings).values({ systemPropmt: input.prompt })
|
||||
}),
|
||||
})
|
||||
@@ -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();
|
||||
@@ -139,4 +139,4 @@
|
||||
* {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user