diff --git a/.changeset/tangy-foxes-float.md b/.changeset/tangy-foxes-float.md new file mode 100644 index 0000000..0fd4ade --- /dev/null +++ b/.changeset/tangy-foxes-float.md @@ -0,0 +1,5 @@ +--- +"commitflow": minor +--- + +workspace feature diff --git a/.env.sample b/.env.sample index 69ea9f9..a6e4449 100644 --- a/.env.sample +++ b/.env.sample @@ -40,4 +40,13 @@ S3_REGION= S3_ACCESS_KEY= S3_SECRET_KEY= # S3 COMPATIBLE STORAGE -S3_ENDPOINT_URL= \ No newline at end of file +S3_ENDPOINT_URL= + +# Mail Server +SMTP_HOST=host.docker.internal +SMTP_PORT=587 +SMTP_USER= +SMTP_PASS= +SMTP_SECURE=false # true untuk SMTPS(465), false untuk STARTTLS (587) +FROM_NAME=CommitFlow +FROM_ADDRESS= \ No newline at end of file diff --git a/README.md b/README.md index c7a7951..36c9973 100644 --- a/README.md +++ b/README.md @@ -36,20 +36,20 @@ With CommitFlow, you can **plan, track, and analyze your projects** β€” all in o A beautiful, AI-assisted workspace for managing your projects and tasks: - πŸ—‚ **Kanban Board** – Organize tasks visually using drag-and-drop. - Each task card shows **priority color accents**, **assignee avatars**, and **due dates**. -- πŸ“‹ **List View** – See all tasks in a clean table layout; click any row to open task details. -- πŸ“† **Timeline View** – A Gantt-style horizontal timeline to visualize progress and overlaps between tasks. -- πŸ’¬ **Task Details Modal** – - - **Rich text editor** (React Quill) for descriptions - - **File attachments** (via AWS S3 integration) - - **Inline comments** with author, timestamp, and preview links -- 🎨 **Smart Selectors** – - - Assignee and Priority fields powered by **React Select**, dynamically colored per user or priority level + Each task card shows **priority color accents**, **assignee avatars**, and **due dates**. +- πŸ“‹ **List View** – See all tasks in a clean table layout; click any row to open task details. +- πŸ“† **Timeline View** – A Gantt-style horizontal timeline to visualize progress and overlaps between tasks. +- πŸ’¬ **Task Details Modal** – + - **Rich text editor** (React Quill) for descriptions + - **File attachments** (via AWS S3 integration) + - **Inline comments** with author, timestamp, and preview links +- 🎨 **Smart Selectors** – + - Assignee and Priority fields powered by **React Select**, dynamically colored per user or priority level - 🧍 **Team Management** – - Add or remove team members using modern UI components, with color-coded avatars automatically generated. -- 🧱 **Project Management Sidebar** – - - Create or delete projects easily - - Integrated **SweetAlert2** confirmations for safe deletions + Add or remove team members using modern UI components, with color-coded avatars automatically generated. +- 🧱 **Project Management Sidebar** – + - Create or delete projects easily + - Integrated **SweetAlert2** confirmations for safe deletions - **Toast notifications** (`react-toastify`) for success actions (e.g., project or member added) - πŸŒ™ **Dark/Light Mode Aware** – Smooth color transitions and well-tuned contrast for both themes. @@ -57,6 +57,7 @@ A beautiful, AI-assisted workspace for managing your projects and tasks: --- ### πŸ“Š Developer Insights + - πŸ“ˆ **GitHub Analytics** – Fetch organization repositories, commits, and contributor stats. - πŸ” **Contribution Breakdown** – Understand who contributes what and when. - πŸ“† **Activity Timeline** – Visualize commit frequency and collaboration trends. @@ -64,6 +65,7 @@ A beautiful, AI-assisted workspace for managing your projects and tasks: --- ### πŸ€– AI-Powered Insights + - πŸ’‘ **AI Recommendations** – Get automatic suggestions for prioritization and sprint planning. - 🧠 **Smart Summaries** – Let AI summarize repository activity and project status. - πŸ—£οΈ **Insight Chatbot** – Ask questions like β€œWho’s most active this week?” or β€œWhich repo grew fastest?” @@ -71,16 +73,18 @@ A beautiful, AI-assisted workspace for managing your projects and tasks: --- ### 🐳 Infrastructure & Security + - 🧩 **PostgreSQL Storage** – Store structured task and analytics data. - πŸ” **Environment Management** – Secure credentials via `.env` file. - βš™οΈ **Docker Ready** – Run everything locally or in production with one command. --- + ## βš™οΈ Requirements -- [Docker](https://www.docker.com/get-started) and Docker Compose -- A **GitHub Personal Access Token** (with `repo` scope) -- An **OpenAI API Key** (for AI Insights & Automation) +- [Docker](https://www.docker.com/get-started) and Docker Compose +- A **GitHub Personal Access Token** (with `repo` scope) +- An **OpenAI API Key** (for AI Insights & Automation) - **AWS S3 Credentials** (for document and image storage) --- @@ -146,14 +150,19 @@ S3_ENDPOINT_URL= --- ### 2. Build and start all containers + For production: + ```bash ./scripts/build.sh ``` + For development (with hot reload and live updates): + ```bash ./scripts/build.dev.sh ``` + --- ### 3. Verify running containers @@ -162,27 +171,27 @@ For development (with hot reload and live updates): docker ps ``` -| Service | URL | -|-----------|------------------------| -| Frontend | http://localhost:3000 | -| Backend | http://localhost:8000 | -| pgAdmin | http://localhost:8080 | +| Service | URL | +| -------- | --------------------- | +| Frontend | http://localhost:3000 | +| Backend | http://localhost:8000 | +| pgAdmin | http://localhost:8080 | --- ### 4. Access pgAdmin -- Email: `admin@example.com` +- Email: `admin@example.com` - Password: `admin123` Then add a new PostgreSQL server: -| Field | Value | -|------------|--------------| -| Host | db | -| Database | commitflow | -| User | postgres | -| Password | password | +| Field | Value | +| -------- | ---------- | +| Host | db | +| Database | commitflow | +| User | postgres | +| Password | password | --- @@ -201,11 +210,13 @@ docker exec -it commitflow-api npx prisma db push ## 🧠 Running the Application **Development (hot reload):** + ```bash NODE_ENV="development" ``` **Production:** + ```bash NODE_ENV="production" ``` @@ -214,6 +225,14 @@ You can switch between development and production using different Docker Compose --- +## Swagger Openapi + +``` +http://localhost:8000/docs +``` + +--- + ## 🀝 Contributing CommitFlow is open source and welcomes contributions! diff --git a/backend/package-lock.json b/backend/package-lock.json index 311f31e..0f9904e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -30,8 +30,11 @@ "dotenv": "^17.2.3", "exceljs": "^4.4.0", "google-tts-api": "^2.0.2", + "handlebars": "^4.7.8", "multer": "^2.0.2", "node-fetch": "^3.3.2", + "nodemailer": "^7.0.10", + "nodemailer-express-handlebars": "^7.0.0", "openai": "^6.7.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", @@ -1653,6 +1656,40 @@ "kuler": "^2.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -2300,7 +2337,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, "license": "MIT", "engines": { "node": "20 || >=22" @@ -2310,7 +2346,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, "license": "MIT", "dependencies": { "@isaacs/balanced-match": "^4.0.1" @@ -2323,7 +2358,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -2341,7 +2375,6 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -2354,14 +2387,12 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -2379,7 +2410,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -3025,6 +3055,19 @@ "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", "license": "MIT" }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, "node_modules/@nestjs/axios": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.1.tgz", @@ -4812,6 +4855,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -5449,6 +5503,261 @@ "dev": true, "license": "ISC" }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@unrs/resolver-binding-win32-x64-msvc": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", @@ -5808,7 +6117,6 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7080,7 +7388,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -7367,7 +7674,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/ecdsa-sig-formatter": { @@ -7987,6 +8293,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-handlebars": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-8.0.3.tgz", + "integrity": "sha512-uzvrS2HRlhFNmq2dZb6EJahu2tg7trV9wuYOJrkIbstemozwOvNRa0idkHBjy62nPu6wao76AlhJ69YBIHfkEA==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "glob": "^11.0.2", + "graceful-fs": "^4.2.11", + "handlebars": "^4.7.8" + }, + "engines": { + "node": ">=22.15.0" + } + }, "node_modules/exsolve": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", @@ -8338,7 +8659,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -8498,6 +8818,21 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/fstream": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", @@ -8652,7 +8987,6 @@ "version": "11.0.3", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.3.1", @@ -8696,7 +9030,6 @@ "version": "10.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", - "dev": true, "license": "ISC", "dependencies": { "@isaacs/brace-expansion": "^5.0.0" @@ -8838,7 +9171,6 @@ "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, "license": "MIT", "dependencies": { "minimist": "^1.2.5", @@ -8860,7 +9192,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -9231,7 +9562,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -9318,7 +9648,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -10885,7 +11214,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -11025,7 +11353,6 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, "license": "MIT" }, "node_modules/node-abort-controller": { @@ -11104,6 +11431,28 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.10.tgz", + "integrity": "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemailer-express-handlebars": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/nodemailer-express-handlebars/-/nodemailer-express-handlebars-7.0.0.tgz", + "integrity": "sha512-fJ5pj5TaDiXKDShzrOksZDR1iE65W8z40unwM8+wiHT3AhifQsc2/G5uw4DC1LNN4RhqXGkuYuoEkoE+Zk+GHQ==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "express-handlebars": ">= 8.0.0", + "nodemailer": ">= 6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -11361,7 +11710,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/pako": { @@ -11471,7 +11819,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -11481,7 +11828,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -11498,7 +11844,6 @@ "version": "11.2.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", - "dev": true, "license": "ISC", "engines": { "node": "20 || >=22" @@ -12289,7 +12634,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -12302,7 +12646,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12384,7 +12727,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -12726,7 +13068,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -12741,7 +13082,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12751,7 +13091,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -12785,7 +13124,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -12802,7 +13140,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -12815,7 +13152,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13646,7 +13982,6 @@ "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, "license": "BSD-2-Clause", "optional": true, "bin": { @@ -14169,7 +14504,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -14258,7 +14592,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi": { @@ -14281,7 +14614,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -14299,7 +14631,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14309,7 +14640,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -14361,29 +14691,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -14533,17 +14840,6 @@ "funding": { "url": "https://github.com/sponsors/isaacs" } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "optional": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } } } } diff --git a/backend/package.json b/backend/package.json index 529db53..8dac526 100644 --- a/backend/package.json +++ b/backend/package.json @@ -41,8 +41,11 @@ "dotenv": "^17.2.3", "exceljs": "^4.4.0", "google-tts-api": "^2.0.2", + "handlebars": "^4.7.8", "multer": "^2.0.2", "node-fetch": "^3.3.2", + "nodemailer": "^7.0.10", + "nodemailer-express-handlebars": "^7.0.0", "openai": "^6.7.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index d41a025..87e3bbb 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -13,37 +13,6 @@ datasource db { provider = "postgresql" url = env("DATABASE_URL") } -model User { - id String @id @default(cuid()) - email String? @unique - phone String? - name String? - password String? // kalau pake password - session_token String? @unique - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // FK ke TeamMember (one-to-one) - teamMemberId String? @unique - teamMember TeamMember? @relation(fields: [teamMemberId], references: [id]) -} - -model TeamMember { - id String @id @default(uuid()) // pilih uuid supaya beda style dari user.id - clientId String? @unique - name String - role String? - email String? @unique - photo String? - phone String? - isTrash Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime? - Task Task[] - - // back relation ke User - user User? @relation -} model aiToolCache { id String @id @default(cuid()) @@ -78,15 +47,63 @@ model chatMessage { @@map("chat_message") } -model Project { - id String @id @default(uuid()) - clientId String? @unique +model User { + id String @id @default(cuid()) + email String? @unique + phone String? + name String? + role Role? @default(USER) + password String? // kalau pake password + session_token String? @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + members TeamMember[] @relation("UserMembers") + +} + +model TeamMember { + id String @id @default(uuid()) // pilih uuid supaya beda style dari user.id + clientId String? @unique + userId String + workspaceId String + name String + role String? + email String? + photo String? + phone String? + isTrash Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime? + Task Task[] + + // FK ke Workspace + workspace Workspace? @relation("WorkspaceMembers", fields: [workspaceId], references: [id]) + user User? @relation("UserMembers", fields: [userId], references: [id]) +} + +model Workspace { + id String @id @default(uuid()) + clientId String? @unique name String description String? - isTrash Boolean @default(false) - createdAt DateTime @default(now()) + isTrash Boolean @default(false) + createdAt DateTime @default(now()) updatedAt DateTime? - tasks Task[] @relation("ProjectTasks") + projects Project[] @relation("WorkspaceProjects") + members TeamMember[] @relation("WorkspaceMembers") +} + +model Project { + id String @id @default(uuid()) + workspaceId String? + clientId String? @unique + name String + description String? + isTrash Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime? + workspace Workspace? @relation("WorkspaceProjects", fields: [workspaceId], references: [id]) + tasks Task[] @relation("ProjectTasks") } model Task { @@ -109,13 +126,18 @@ model Task { } model Comment { - id String @id @default(uuid()) + id String @id @default(uuid()) taskId String - task Task @relation("TaskComments", fields: [taskId], references: [id]) + task Task @relation("TaskComments", fields: [taskId], references: [id]) author String body String attachments Json? // optional JSON array of attachments metadata isTrash Boolean @default(false) - createdAt DateTime @default(now()) + createdAt DateTime @default(now()) updatedAt DateTime? +} + +enum Role { + USER + ADMIN } \ No newline at end of file diff --git a/backend/src/ai-agent/ask.service.ts b/backend/src/ai-agent/ask.service.ts index cff1f24..edf65b5 100644 --- a/backend/src/ai-agent/ask.service.ts +++ b/backend/src/ai-agent/ask.service.ts @@ -71,32 +71,32 @@ export class AskService { * Helper: execute a tool by name (serial) * Returns the raw tool result (JS object/array) or { error: ... } */ - private async execToolByName(fn: string, args: any) { + private async execToolByName(fn: string, args: any, userId: string) { try { if (fn === "getRepos") { return await getRepos(); } else if (fn === "getContributors") { return await getContributors(args.repo); } else if (fn === "getProjects") { - return await getProjects(); + return await getProjects(userId); } else if (fn === "getMembers") { - return await getMembers(); + return await getMembers(userId); } else if (fn === "getAllTasks") { - return await getAllTasks(args?.projectId || ""); + return await getAllTasks(args?.projectId || "", userId); } else if (fn === "getTodoTasks") { - return await getTodoTasks(args?.projectId || ""); + return await getTodoTasks(args?.projectId || "", userId); } else if (fn === "getInProgressTasks") { - return await getInProgressTasks(args?.projectId || ""); + return await getInProgressTasks(args?.projectId || "", userId); } else if (fn === "getDoneTasks") { - return await getDoneTasks(args?.projectId || ""); + return await getDoneTasks(args?.projectId || "", userId); } else if (fn === "getUnassignedTasks") { - return await getUnassignedTasks(args?.projectId || ""); + return await getUnassignedTasks(args?.projectId || "", userId); } else if (fn === "getUrgentTasks") { - return await getUrgentTasks(args?.projectId || ""); + return await getUrgentTasks(args?.projectId || "", userId); } else if (fn === "getLowTasks") { - return await getLowTasks(args?.projectId || ""); + return await getLowTasks(args?.projectId || "", userId); } else if (fn === "getMediumTasks") { - return await getMediumTasks(args?.projectId || ""); + return await getMediumTasks(args?.projectId || "", userId); } else { return { error: `Unknown tool: ${fn}` }; } @@ -223,7 +223,7 @@ export class AskService { } else { // Execute actual tool this.emitToolCall(socket, fn, args); - toolResult = await this.execToolByName(fn, args); + toolResult = await this.execToolByName(fn, args, userId ?? ""); this.emitToolResult(socket, fn, toolResult); // push tool result into conversation diff --git a/backend/src/ai-agent/project.service.ts b/backend/src/ai-agent/project.service.ts index 922ea0c..f5dd407 100644 --- a/backend/src/ai-agent/project.service.ts +++ b/backend/src/ai-agent/project.service.ts @@ -9,12 +9,23 @@ const prisma = new PrismaClient(); * PROJECTS * ===================================== */ -export async function getProjects() { +export async function getProjects(userId: string) { try { console.log("getProjects"); + const team = await prisma.teamMember.findMany({ + where: { + userId, + }, + }); + const workspaceIds = team.map((item: any) => item.workspaceId); const results = await prisma.project.findMany({ - where: { isTrash: false }, + where: { + isTrash: false, + workspaceId: { + in: workspaceIds, + }, + }, include: { tasks: { where: { isTrash: false }, @@ -79,9 +90,37 @@ function baseTaskInclude() { }; } -function buildTaskWhere(projectId?: string, status?: string) { +async function buildTaskWhere( + userId: string, + projectId?: string, + status?: string +) { + const team = await prisma.teamMember.findMany({ + where: { + userId, + }, + }); + const workspaceIds = team.map((item: any) => item.workspaceId); + const whereProject: any = { + workspaceId: { + in: workspaceIds, + }, + }; + if (projectId) { + whereProject.id = projectId; + } + + const projects = await prisma.project.findMany({ + where: whereProject, + }); + + const projectIds = projects.map((item: any) => item.id); + const where: any = { isTrash: false }; - if (projectId) where.projectId = projectId; + if (projectId) + where.projectId = { + in: projectIds, + }; if (status) where.status = status; return where; } @@ -91,12 +130,12 @@ function buildTaskWhere(projectId?: string, status?: string) { * ALL TASKS * ===================================== */ -export async function getAllTasks(projectId = "") { +export async function getAllTasks(projectId = "", userId: string) { try { console.log("getAllTasks"); const results = await prisma.task.findMany({ - where: buildTaskWhere(projectId), + where: await buildTaskWhere(userId, projectId), include: baseTaskInclude(), orderBy: { createdAt: "desc" }, }); @@ -113,12 +152,12 @@ export async function getAllTasks(projectId = "") { * TODO TASKS * ===================================== */ -export async function getTodoTasks(projectId = "") { +export async function getTodoTasks(projectId = "", userId: string) { try { console.log("getTodoTasks"); const results = await prisma.task.findMany({ - where: buildTaskWhere(projectId, "todo"), + where: await buildTaskWhere(userId, projectId, "todo"), include: baseTaskInclude(), orderBy: { createdAt: "desc" }, }); @@ -135,12 +174,12 @@ export async function getTodoTasks(projectId = "") { * IN PROGRESS TASKS * ===================================== */ -export async function getInProgressTasks(projectId = "") { +export async function getInProgressTasks(projectId = "", userId: string) { try { console.log("getInProgressTasks"); const results = await prisma.task.findMany({ - where: buildTaskWhere(projectId, "inprogress"), + where: await buildTaskWhere(userId, projectId, "inprogress"), include: baseTaskInclude(), orderBy: { createdAt: "desc" }, }); @@ -157,12 +196,12 @@ export async function getInProgressTasks(projectId = "") { * DONE TASKS * ===================================== */ -export async function getDoneTasks(projectId = "") { +export async function getDoneTasks(projectId = "", userId: string) { try { console.log("getDoneTasks"); const results = await prisma.task.findMany({ - where: buildTaskWhere(projectId, "done"), + where: await buildTaskWhere(userId, projectId, "done"), include: baseTaskInclude(), orderBy: { createdAt: "desc" }, }); @@ -179,7 +218,7 @@ export async function getDoneTasks(projectId = "") { * MEMBERS * ===================================== */ -export async function getMembers() { +export async function getMembers(userId: string) { try { console.log("getMembers"); @@ -229,7 +268,10 @@ export async function getMembers() { } } -export async function getUnassignedTasks(projectId: string = "") { +export async function getUnassignedTasks( + projectId: string = "", + userId: string +) { try { console.log("getUnassignedTasks"); @@ -253,7 +295,7 @@ export async function getUnassignedTasks(projectId: string = "") { } } -export async function getUrgentTasks(projectId: string = "") { +export async function getUrgentTasks(projectId: string = "", userId: string) { try { console.log("getUrgentTasks"); @@ -277,7 +319,7 @@ export async function getUrgentTasks(projectId: string = "") { } } -export async function getLowTasks(projectId: string = "") { +export async function getLowTasks(projectId: string = "", userId: string) { try { console.log("getLowTasks"); @@ -301,7 +343,7 @@ export async function getLowTasks(projectId: string = "") { } } -export async function getMediumTasks(projectId: string = "") { +export async function getMediumTasks(projectId: string = "", userId: string) { try { console.log("getMediumTasks"); diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 6a02ead..144204b 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -15,6 +15,7 @@ import { JwtGuard } from "./common/guards/jwt.guard"; import { SharedModule } from "./common/shared.module"; import { UploadModule } from './upload/upload.module'; import { ProjectManagementModule } from './project-management/project-management.module'; +import { EmailModule } from './email/email.module'; @Module({ imports: [ @@ -36,6 +37,7 @@ import { ProjectManagementModule } from './project-management/project-management SharedModule, UploadModule, ProjectManagementModule, + EmailModule, ], controllers: [AppController, AskController], providers: [AppService, AskService, AskGateway, JwtGuard], diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index 83f6d76..fb0c088 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -23,6 +23,8 @@ export class AuthController { return { token: result.token, userId: result.user.id, + user: result.user, + workspaceId: result.workspace.id, teamMemberId: result.teamMember.id, clientTempId: result.clientTempId ?? null, }; @@ -36,7 +38,8 @@ export class AuthController { return { token: result.token, userId: result?.user?.id ?? "", - teamMemberId: result.teamMember?.id ?? null, + user: result.user, + teamMemberId: result?.teamMemberId, }; } } diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index 7fc6fc8..a5d37cf 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -48,7 +48,7 @@ export class AuthService { } async register(dto: RegisterDto) { - const { clientTempId, email, name, role, photo, password } = dto; + const { clientTempId, workspace, email, name, role, photo, password } = dto; // Basic uniqueness check const existing = await this.prisma.user.findUnique({ where: { email } }); @@ -56,32 +56,49 @@ export class AuthService { // Use transaction to create TeamMember then User referencing member.id try { + console.log(workspace); const result = await this.prisma.$transaction(async (tx) => { - // 1) create TeamMember first - const teamMember = await tx.teamMember.create({ + // 1) create Workspace first + const createWorkspace = await tx.workspace.create({ data: { - name, - email, - role: role ?? null, - photo: photo ?? null, - // optionally store clientTempId if you want mapping - // clientTempId: clientTempId ?? null // uncomment if you added field + name: workspace, createdAt: new Date(), }, }); - // 2) create User referencing teamMember.id + // 2) create User referencing const hashed = password ? hashPassword(password) : undefined; const user = await tx.user.create({ data: { email, name, password: hashed, - teamMemberId: teamMember.id, // <-- link via member id + }, + select: { + id: true, + name: true, + phone: true, + email: true, + members: true, }, }); - return { user, teamMember }; + // 3) create TeamMember + const teamMember = await tx.teamMember.create({ + data: { + userId: user.id, + workspaceId: createWorkspace.id, + name, + email, + role: role ?? null, + photo: photo ?? null, + // optionally store clientTempId if you want mapping + // clientTempId: clientTempId ?? null // uncomment if you added field + createdAt: new Date(), + }, + }); + + return { user, teamMember, workspace: createWorkspace }; }); // Create JWT (you can include teamMemberId in payload too) @@ -100,6 +117,8 @@ export class AuthService { token: jwt, user: result.user, teamMember: result.teamMember, + teamMemberId: result.teamMember.id, + workspace: result.workspace, // optionally map clientTempId => memberId so FE can replace optimistic id clientTempId, }; @@ -116,9 +135,16 @@ export class AuthService { if (!email) throw new UnauthorizedException("Invalid credentials"); // find user and include relation if exists - let user = await this.prisma.user.findUnique({ + const user = await this.prisma.user.findUnique({ where: { email }, - include: { teamMember: true }, + select: { + id: true, + name: true, + phone: true, + email: true, + members: true, + password: true, + }, }); if (!user) throw new UnauthorizedException("Invalid credentials"); @@ -127,34 +153,16 @@ export class AuthService { const ok = comparePassword(password || "", user.password); if (!ok) throw new UnauthorizedException("Invalid credentials"); - // If relation missing, try to find TeamMember by email and attach it (best-effort) - let teamMember = (user as any).teamMember ?? null; - if (!teamMember) { - if (user.email) { - const found = await this.prisma.teamMember.findUnique({ - where: { email: user.email }, - }); - if (found) { - // attach FK so next time it's present - try { - await this.prisma.user.update({ - where: { id: user.id }, - data: { teamMemberId: found.id }, - }); - teamMember = found; - // also reflect in local user object - user = { ...user, teamMemberId: found.id } as any; - } catch (err) { - // ignore update failure (concurrency/unique) β€” still continue - console.warn("Failed to attach teamMemberId:", err); - } - } - } - } + // find user and include relation if exists + const teamMember: any = await this.prisma.teamMember.findFirst({ + where: { email }, + }); + + if (!teamMember) throw new UnauthorizedException("Invalid credentials"); const jwt = this.jwtService.sign({ userId: user?.id, - teamMemberId: teamMember?.id ?? null, + teamMemberId: teamMember.id, }); await this.prisma.user.update({ @@ -162,7 +170,13 @@ export class AuthService { data: { session_token: jwt }, }); - return { token: jwt, user, teamMember }; + // Ensure password property is possibly undefined before delete, and avoid type error + if ("password" in user) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + delete (user as any).password; + } + + return { token: jwt, user, teamMember, teamMemberId: teamMember.id }; } async validateUser(token: string) { diff --git a/backend/src/auth/dto/register.dto.ts b/backend/src/auth/dto/register.dto.ts index 7c20b8a..bd66d6a 100644 --- a/backend/src/auth/dto/register.dto.ts +++ b/backend/src/auth/dto/register.dto.ts @@ -6,6 +6,9 @@ export class RegisterDto { @IsString() clientTempId?: string; // optional, FE optimistic id + @IsString() + workspace: string; + @IsEmail() email: string; diff --git a/backend/src/email/dto/send-email.dto.ts b/backend/src/email/dto/send-email.dto.ts new file mode 100644 index 0000000..b4d578c --- /dev/null +++ b/backend/src/email/dto/send-email.dto.ts @@ -0,0 +1,15 @@ +import { IsEmail, IsNotEmpty, IsOptional } from "class-validator"; + +export class SendEmailDto { + @IsEmail() + to: string; + + @IsNotEmpty() + subject: string; + + @IsOptional() + text?: string; + + @IsOptional() + html?: string; +} diff --git a/backend/src/email/email.controller.spec.ts b/backend/src/email/email.controller.spec.ts new file mode 100644 index 0000000..9470aa8 --- /dev/null +++ b/backend/src/email/email.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EmailController } from './email.controller'; + +describe('EmailController', () => { + let controller: EmailController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [EmailController], + }).compile(); + + controller = module.get(EmailController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/email/email.controller.ts b/backend/src/email/email.controller.ts new file mode 100644 index 0000000..7fc0058 --- /dev/null +++ b/backend/src/email/email.controller.ts @@ -0,0 +1,19 @@ +import { Body, Controller, Post } from "@nestjs/common"; +import { EmailService } from "./email.service"; +import { SendEmailDto } from "./dto/send-email.dto"; + +@Controller("email") +export class EmailController { + constructor(private readonly emailService: EmailService) {} + + @Post("send") + async send(@Body() dto: SendEmailDto) { + const res = await this.emailService.sendMail({ + to: dto.to, + subject: dto.subject, + text: dto.text, + html: dto.html, + }); + return res; + } +} diff --git a/backend/src/email/email.module.ts b/backend/src/email/email.module.ts new file mode 100644 index 0000000..be37715 --- /dev/null +++ b/backend/src/email/email.module.ts @@ -0,0 +1,10 @@ +import { Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { EmailService } from "./email.service"; + +@Module({ + imports: [ConfigModule], + providers: [EmailService], + exports: [EmailService], +}) +export class EmailModule {} diff --git a/backend/src/email/email.service.spec.ts b/backend/src/email/email.service.spec.ts new file mode 100644 index 0000000..27719da --- /dev/null +++ b/backend/src/email/email.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EmailService } from './email.service'; + +describe('EmailService', () => { + let service: EmailService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [EmailService], + }).compile(); + + service = module.get(EmailService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/email/email.service.ts b/backend/src/email/email.service.ts new file mode 100644 index 0000000..2746f77 --- /dev/null +++ b/backend/src/email/email.service.ts @@ -0,0 +1,97 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import * as nodemailer from "nodemailer"; +import Mail from "nodemailer/lib/mailer"; + +export type SendMailOptions = { + to: string | string[]; + subject: string; + text?: string; + html?: string; + from?: string; + cc?: string | string[]; + bcc?: string | string[]; + attachments?: Mail.Attachment[]; +}; + +@Injectable() +export class EmailService { + private readonly logger = new Logger(EmailService.name); + private transporter: Mail; + + constructor(private readonly config: ConfigService) { + const host = this.config.get("SMTP_HOST", "127.0.0.1"); + const port = Number(this.config.get("SMTP_PORT", "587")); + const user = this.config.get("SMTP_USER"); + const pass = this.config.get("SMTP_PASS"); + const secure = this.config.get("SMTP_SECURE", "false") === "true"; + + const auth = user && pass ? { user, pass } : undefined; + + this.transporter = nodemailer.createTransport({ + host, + port, + secure, + auth, + tls: { + // Accept self-signed certs if you run local test servers (set false in prod). + rejectUnauthorized: + this.config.get("SMTP_REJECT_UNAUTHORIZED", "true") === + "true", + }, + // Connection timeout defaults can be set here if needed: + // connectionTimeout: 30_000, + }); + + // verify transporter early so any config error surfaces + this.transporter + .verify() + .then(() => { + this.logger.log( + `SMTP transporter connected: ${host}:${port} (secure=${secure})` + ); + }) + .catch((err) => { + this.logger.warn( + "SMTP transporter verification failed: " + err?.message + ); + }); + } + + private defaultFrom(): string { + const name = this.config.get("FROM_NAME") || ""; + const addr = + this.config.get("FROM_ADDRESS") || + this.config.get("SMTP_USER") || + "no-reply@example.com"; + return name ? `"${name}" <${addr}>` : addr; + } + + async sendMail(opts: SendMailOptions) { + const from = opts.from ?? this.defaultFrom(); + + const mail: Mail.Options = { + from, + to: opts.to, + subject: opts.subject, + text: opts.text, + html: opts.html, + cc: opts.cc, + bcc: opts.bcc, + attachments: opts.attachments, + }; + + try { + const info = await this.transporter.sendMail(mail); + this.logger.log( + `Email sent: ${info.messageId} to ${ + Array.isArray(opts.to) ? opts.to.join(",") : opts.to + }` + ); + return { ok: true, info }; + } catch (error) { + this.logger.error("Failed to send email: " + (error?.message ?? error)); + return { ok: false, error }; + } + } +} diff --git a/backend/src/project-management/dto/project.dto.ts b/backend/src/project-management/dto/project.dto.ts index efe37e1..d82ea11 100644 --- a/backend/src/project-management/dto/project.dto.ts +++ b/backend/src/project-management/dto/project.dto.ts @@ -1,20 +1,26 @@ -import { IsOptional, IsString } from 'class-validator'; +import { IsOptional, IsString } from "class-validator"; export class CreateProjectDto { - @IsString() - name: string; + @IsString() + name: string; + @IsString() + workspaceId: string; - @IsOptional() - @IsString() - description?: string; + @IsOptional() + @IsString() + clientId?: string | null; + + @IsOptional() + @IsString() + description?: string; } export class UpdateProjectDto { - @IsOptional() - @IsString() - name?: string; + @IsOptional() + @IsString() + name?: string; - @IsOptional() - @IsString() - description?: string; + @IsOptional() + @IsString() + description?: string; } diff --git a/backend/src/project-management/dto/team.dto.ts b/backend/src/project-management/dto/team.dto.ts index a394b97..3718e1d 100644 --- a/backend/src/project-management/dto/team.dto.ts +++ b/backend/src/project-management/dto/team.dto.ts @@ -23,6 +23,9 @@ export class CreateTeamMemberDto { @IsOptional() @IsString() photo?: string; + + @IsString() + workspaceId: string; } export class UpdateTeamMemberDto { @@ -34,10 +37,6 @@ export class UpdateTeamMemberDto { @IsString() role?: string; - @IsOptional() - @IsString() - email?: string; - @IsOptional() @IsString() password?: string; diff --git a/backend/src/project-management/dto/workspace.dto.ts b/backend/src/project-management/dto/workspace.dto.ts new file mode 100644 index 0000000..3351eb3 --- /dev/null +++ b/backend/src/project-management/dto/workspace.dto.ts @@ -0,0 +1,20 @@ +import { IsOptional, IsString } from "class-validator"; + +export class CreateWorkspaceDto { + @IsString() + name: string; + + @IsOptional() + @IsString() + description?: string; +} + +export class UpdateWorkspaceDto { + @IsOptional() + @IsString() + name?: string; + + @IsOptional() + @IsString() + description?: string; +} diff --git a/backend/src/project-management/project-management.controller.spec.ts b/backend/src/project-management/project-management.controller.spec.ts index 6d89e3d..e679cc3 100644 --- a/backend/src/project-management/project-management.controller.spec.ts +++ b/backend/src/project-management/project-management.controller.spec.ts @@ -1,7 +1,7 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ProjectManagementController } from './project-management.controller'; +import { Test, TestingModule } from "@nestjs/testing"; +import { ProjectManagementController } from "./project-management.controller"; -describe('ProjectManagementController', () => { +describe("ProjectManagementController", () => { let controller: ProjectManagementController; beforeEach(async () => { @@ -9,10 +9,12 @@ describe('ProjectManagementController', () => { controllers: [ProjectManagementController], }).compile(); - controller = module.get(ProjectManagementController); + controller = module.get( + ProjectManagementController + ); }); - it('should be defined', () => { + it("should be defined", () => { expect(controller).toBeDefined(); }); }); diff --git a/backend/src/project-management/project-management.controller.ts b/backend/src/project-management/project-management.controller.ts index fa0a401..6c335df 100644 --- a/backend/src/project-management/project-management.controller.ts +++ b/backend/src/project-management/project-management.controller.ts @@ -15,6 +15,7 @@ import { BadRequestException, StreamableFile, UseGuards, + Req, } from "@nestjs/common"; import { FileInterceptor } from "@nestjs/platform-express"; import type { Response } from "express"; @@ -27,16 +28,23 @@ import { Request } from "express"; import * as fs from "fs"; import { join } from "path"; import { JwtGuard } from "src/common/guards/jwt.guard"; +import { CreateWorkspaceDto } from "./dto/workspace.dto"; @Controller("api") @UseGuards(JwtGuard) export class ProjectManagementController { constructor(private svc: ProjectManagementService) {} + @Get("project-management/workspaces") + getWorkspaces(@Req() req: any) { + const userId = req.user.userId; // <-- ambil userId dari JWT + return this.svc.getWorkspaces(userId); + } + // State / bootstrap - @Get("project-management/state") - getState() { - return this.svc.getState(); + @Get("project-management/state/:id") + getState(@Param("id") id: string) { + return this.svc.getState(id); } // Export XLSX @@ -67,10 +75,17 @@ export class ProjectManagementController { return result; } + // Workspaces + @Post("workspaces") + createWorkspaces(@Body() dto: CreateWorkspaceDto, @Req() req: any) { + const userId = req.user.userId; // <-- ambil userId dari JWT + return this.svc.createWorkspaces(dto, userId); + } + // Projects - @Get("projects") - getProjects() { - return this.svc.getProjects(); + @Get("projects/:id") + getProjects(@Param("id") id: string) { + return this.svc.getProjects(id); } @Post("projects") @@ -100,13 +115,23 @@ export class ProjectManagementController { } @Put("tasks/:id") - updateTask(@Param("id") id: string, @Body() dto: UpdateTaskDto) { - return this.svc.updateTask(id, dto); + updateTask( + @Param("id") id: string, + @Body() dto: UpdateTaskDto, + @Req() req: any + ) { + const userId = req.user.userId; + return this.svc.updateTask(id, dto, userId); } @Patch("tasks/:id") - patchTask(@Param("id") id: string, @Body() patch: PatchTaskDto) { - return this.svc.patchTask(id, patch); + patchTask( + @Param("id") id: string, + @Body() patch: PatchTaskDto, + @Req() req: any + ) { + const userId = req.user.userId; + return this.svc.patchTask(id, patch, userId); } @Delete("tasks/:id") @@ -146,9 +171,9 @@ export class ProjectManagementController { } // Team members - @Get("team") - getTeam() { - return this.svc.getTeam(); + @Get("team/:id") + getTeam(@Param("id") id: string) { + return this.svc.getTeam(id); } @Post("team") diff --git a/backend/src/project-management/project-management.module.ts b/backend/src/project-management/project-management.module.ts index 3f9c129..f731199 100644 --- a/backend/src/project-management/project-management.module.ts +++ b/backend/src/project-management/project-management.module.ts @@ -1,16 +1,18 @@ -import { Module } from '@nestjs/common'; -import { ProjectManagementController } from './project-management.controller'; -import { ProjectManagementService } from './project-management.service'; -import { MulterModule } from '@nestjs/platform-express'; +import { Module } from "@nestjs/common"; +import { ProjectManagementController } from "./project-management.controller"; +import { ProjectManagementService } from "./project-management.service"; +import { MulterModule } from "@nestjs/platform-express"; +import { EmailModule } from "src/email/email.module"; @Module({ imports: [ MulterModule.register({ - dest: './uploads', + dest: "./uploads", }), + EmailModule, ], controllers: [ProjectManagementController], providers: [ProjectManagementService], exports: [ProjectManagementService], }) -export class ProjectManagementModule { } +export class ProjectManagementModule {} diff --git a/backend/src/project-management/project-management.service.ts b/backend/src/project-management/project-management.service.ts index 833ce12..b28287e 100644 --- a/backend/src/project-management/project-management.service.ts +++ b/backend/src/project-management/project-management.service.ts @@ -10,20 +10,47 @@ import * as ExcelJS from "exceljs"; import * as fs from "fs"; import { Prisma } from "@prisma/client"; import { hashPassword } from "src/auth/utils"; +import { EmailService } from "src/email/email.service"; +import logger from "vico-logger"; const prisma = new PrismaClient(); @Injectable() export class ProjectManagementService { + constructor(private email: EmailService) {} + async getWorkspaces(userId) { + const members = await prisma.teamMember.findMany({ + where: { isTrash: false, userId }, + orderBy: { createdAt: "asc" }, + }); + + const workspaces = await prisma.workspace.findMany({ + where: { + isTrash: false, + id: { + in: members.map((item: any) => item.workspaceId), + }, + }, + orderBy: { createdAt: "asc" }, + }); + + return workspaces; + } + // state - async getState() { + async getState(workspaceId) { const projects = await prisma.project.findMany({ - where: { isTrash: false }, + where: { isTrash: false, workspaceId }, orderBy: { createdAt: "desc" }, }); const tasks = await prisma.task.findMany({ - where: { isTrash: false }, + where: { + isTrash: false, + projectId: { + in: projects.map((item: any) => item.id), + }, + }, orderBy: { createdAt: "desc" }, include: { comments: { @@ -34,7 +61,10 @@ export class ProjectManagementService { }); const team = await prisma.teamMember.findMany({ - where: { isTrash: false }, + where: { + isTrash: false, + workspaceId, + }, orderBy: { name: "asc" }, }); @@ -58,11 +88,58 @@ export class ProjectManagementService { }; } + // Workspaces + + async createWorkspaces( + payload: { + name: string; + description?: string | null; + clientId?: string | null; + }, + userId + ) { + // optional idempotency by clientId (if you add unique constraint later) + if (payload.clientId) { + const existing = await prisma.workspace.findFirst({ + where: { clientId: payload.clientId }, + }); + if (existing) return existing; + } + + const p = await prisma.workspace.create({ + data: { + name: payload.name ?? "Untitled", + description: payload.description ?? null, + clientId: payload.clientId ?? null, + }, + }); + + const user = await prisma.user.findFirst({ + where: { + id: userId, + }, + }); + if (user) { + const createTeam = await prisma.teamMember.create({ + data: { + clientId: payload.clientId ?? null, + userId, + workspaceId: p.id, + name: user?.name ?? "", + email: user?.email ?? null, + phone: user?.phone ?? null, + }, + }); + } + return p; + } + // Projects - async getProjects() { + async getProjects(workspaceId) { const projects = await prisma.project.findMany({ where: { isTrash: false, + workspaceId, }, orderBy: { createdAt: "desc" }, }); @@ -73,6 +150,7 @@ export class ProjectManagementService { name: string; description?: string; clientId?: string | null; + workspaceId: string; }) { // optional idempotency by clientId (if you add unique constraint later) if (payload.clientId) { @@ -87,8 +165,78 @@ export class ProjectManagementService { name: payload.name ?? "Untitled", description: payload.description ?? null, clientId: payload.clientId ?? null, + workspaceId: payload.workspaceId ?? null, }, }); + + //send email to team members + const teams = await prisma.teamMember.findMany({ + where: { workspaceId: payload.workspaceId, isTrash: false }, + select: { email: true }, + }); + + const toEmails = teams.map((t) => t.email?.trim()).filter(Boolean); + + if (toEmails.length === 0) throw new Error("No recipient emails found"); + + const projectName = p.name; + const projectDesc = p.description ?? "No description provided."; + + const textMsg = ` + A new project has been created on CommitFlow + + Project Name: + ${projectName} + + Description: + ${projectDesc} + + You are receiving this notification because you are part of the workspace team. + + Regards, + CommitFlow Team + `; + + const htmlMsg = ` +
+

πŸš€ New Project Created

+

A new project has been added to your workspace on CommitFlow.

+ +
+

+ Project Name:
+ ${projectName} +

+ +

+ Description:
+ ${projectDesc} +

+
+ +

You are receiving this because you are a member of this workspace.

+ +

+ β€” CommitFlow Team +

+
+ `; + + for (const recipient of toEmails) { + try { + await this.email.sendMail({ + to: recipient ?? "getechindonesia@gmail.com", + subject: "New Project Created | CommitFlow", + text: textMsg, + html: htmlMsg, + }); + } catch (error) { + logger.error(error); + } + + await new Promise((r) => setTimeout(r, 200)); + } + return p; } @@ -241,6 +389,7 @@ export class ProjectManagementService { "clientId=", payload.clientId ?? null ); + // return serialized created task (timestamps as ISO) return { ...t, @@ -260,7 +409,8 @@ export class ProjectManagementService { assigneeId?: string | null; startDate?: string | null; dueDate?: string | null; - }> + }>, + userId: string ) { const exists = await prisma.task.findUnique({ where: { id } }); if (!exists) throw new NotFoundException("Task not found"); @@ -275,7 +425,7 @@ export class ProjectManagementService { }); if (!p) throw new NotFoundException("Project not found"); } - + let assignee: any = null; // validate assignee if present and not null if ( typeof payload.assigneeId !== "undefined" && @@ -285,6 +435,7 @@ export class ProjectManagementService { where: { id: payload.assigneeId }, }); if (!m) throw new NotFoundException("Assignee not found"); + assignee = m; } // build clean data object with allowed fields only @@ -327,6 +478,110 @@ export class ProjectManagementService { }, }); + //send email to team members + const team = await prisma.teamMember.findFirst({ + where: { userId, isTrash: false }, + }); + + if (!team) throw new NotFoundException("Team not found"); + + const teams = await prisma.teamMember.findMany({ + where: { workspaceId: team.workspaceId, isTrash: false }, + select: { email: true }, + }); + + // Ambil nama project (optional, tapi lebih keren) + const project = await prisma.project.findFirst({ + where: { id: payload.projectId ?? "" }, + select: { name: true }, + }); + + if (!project) throw new NotFoundException("Project not found"); + + const projectName = project?.name ?? "No Project"; + + const toEmails = teams.map((t) => t.email?.trim()).filter(Boolean); + + if (toEmails.length === 0) throw new Error("No recipient emails found"); + // Format tanggal + const format = (d: any) => + d ? new Date(d).toLocaleDateString("en-US") : "β€”"; + + const textMsg = ` + A new task has been created on CommitFlow + + Task Title: + ${updated.title} + + Description: + ${updated.description ?? "No description"} + + Status: ${updated.status} + Assignee: ${assignee.name ?? "none"} + Priority: ${updated.priority ?? "none"} + Start Date: ${format(updated.startDate)} + Due Date: ${format(updated.dueDate)} + + Project: + ${projectName} + + Regards, + CommitFlow Team + `; + + const htmlMsg = ` +
+

πŸ“ New Task Created

+

A new task has been created on CommitFlow.

+ +
+

+ Task Title:
+ ${updated.title} +

+ +

+ Description:
+ ${updated.description ?? "No description"} +

+ +

+ Status: ${updated.status}
+ Assignee: ${assignee.name ?? "none"}
+ Priority: ${updated.priority ?? "none"} +

+ +

+ Start Date: ${format(updated.startDate)}
+ Due Date: ${format(updated.dueDate)} +

+ +

+ Project:
+ ${projectName} +

+
+ +

You received this because you are a member of the workspace team.

+ +

+ β€” CommitFlow Team +

+
+ `; + + // KIRIM EMAIL + for (const recipient of toEmails) { + await this.email.sendMail({ + to: recipient ?? "getechindonesia@gmail.com", + subject: "New Task Created | CommitFlow", + text: textMsg, + html: htmlMsg, + }); + + await new Promise((r) => setTimeout(r, 200)); + } + // serialize timestamps return { ...withComments, @@ -351,10 +606,11 @@ export class ProjectManagementService { assigneeId?: string | null; startDate?: string | null; dueDate?: string | null; - }> + }>, + userId: string ) { // reuse updateTask logic (keeps validations & logging) - return this.updateTask(id, patch); + return this.updateTask(id, patch, userId); } async deleteTask(id: string) { @@ -443,10 +699,11 @@ export class ProjectManagementService { } // Team - async getTeam() { + async getTeam(workspaceId) { return await prisma.teamMember.findMany({ where: { isTrash: false, + workspaceId, }, orderBy: { name: "asc" }, }); @@ -461,52 +718,47 @@ export class ProjectManagementService { phone?: string; password?: string; clientId?: string | null; + workspaceId: string; }> ) { - const { clientId, email, name, role, photo, phone, password } = + const { clientId, email, name, role, photo, phone, password, workspaceId } = payload as any; // 1) If clientId provided β€” try to find existing TeamMember by clientId first. if (clientId) { const existing = await prisma.teamMember.findFirst({ - where: { clientId }, + where: { clientId, workspaceId }, }); if (existing) { - // Make sure a corresponding User exists and is linked; if not, create it. - let user = await prisma.user.findFirst({ - where: { teamMemberId: existing.id }, - }); - - // If user not exists but email provided, create user (hash password if provided) - if (!user) { - const hashed = password ? hashPassword(password) : undefined; - try { - user = await prisma.user.create({ - data: { - name: name ?? existing.name ?? "Unnamed", - email: email ?? existing.email ?? null, - password: hashed ?? null, - phone: phone ?? null, - teamMemberId: existing.id, - }, - }); - } catch (err: any) { - // Unique constraint / other errors might occur β€” ignore or rethrow sensible error - if (err?.code === "P2002") { - throw new ConflictException("Email already used"); - } - console.error("createTeamMember (attach user) failed", err); - throw new InternalServerErrorException("Failed to create user"); - } - } - - return { teamMember: existing, user }; + return { teamMember: existing }; } } // 2) Otherwise create both inside a transaction (atomic) try { const result = await prisma.$transaction(async (tx) => { + //check user + let user: any = null; + const existingUser = await prisma.user.findFirst({ + where: { email }, + }); + if (existingUser) { + user = existingUser; + } + + if (!user) { + // create user + const hashed = password ? hashPassword(password) : undefined; + user = await tx.user.create({ + data: { + name: name ?? "Unnamed", + email: email ?? null, + password: hashed ?? null, + phone: phone ?? null, + }, + }); + } + console.log(user); // create team member const tm = await tx.teamMember.create({ data: { @@ -516,22 +768,12 @@ export class ProjectManagementService { photo: photo ?? null, phone: phone ?? null, clientId: clientId ?? null, + userId: user.id, + workspaceId, createdAt: new Date(), }, }); - // create user referencing teamMemberId - const hashed = password ? hashPassword(password) : undefined; - const user = await tx.user.create({ - data: { - name: name ?? tm.name ?? "Unnamed", - email: email ?? null, - password: hashed ?? null, - phone: phone ?? null, - teamMemberId: tm.id, - }, - }); - return { tm, user }; }); @@ -554,7 +796,6 @@ export class ProjectManagementService { payload: Partial<{ name?: string; role?: string; - email?: string; phone?: string; password?: string; photo?: string; @@ -568,7 +809,6 @@ export class ProjectManagementService { const tmData: any = {}; if (typeof payload.name !== "undefined") tmData.name = payload.name; if (typeof payload.role !== "undefined") tmData.role = payload.role; - if (typeof payload.email !== "undefined") tmData.email = payload.email; if (typeof payload.phone !== "undefined") tmData.phone = payload.phone; if (typeof payload.photo !== "undefined") tmData.photo = payload.photo; tmData.updatedAt = new Date(); @@ -576,8 +816,6 @@ export class ProjectManagementService { // prepare user update/create data const userData: any = {}; if (typeof payload.name !== "undefined") userData.name = payload.name; - if (typeof payload.email !== "undefined") - userData.email = payload.email ?? null; if (typeof payload.phone !== "undefined") userData.phone = payload.phone ?? null; // password must be hashed @@ -594,7 +832,9 @@ export class ProjectManagementService { }); // 3) find existing user linked to this teamMember - let user = await tx.user.findFirst({ where: { teamMemberId: id } }); + let user = await tx.user.findFirst({ + where: { email: updatedTeam.email }, + }); if (user) { // update existing user (only fields present) @@ -606,27 +846,6 @@ export class ProjectManagementService { data: userData, }); } - } else { - // no linked user exists β€” create one only if email or name or password present - // otherwise skip creating user and return null user - if ( - userData.email || - userData.name || - typeof userData.password !== "undefined" || - typeof userData.phone !== "undefined" - ) { - user = await tx.user.create({ - data: { - name: userData.name ?? updatedTeam.name ?? "Unnamed", - email: userData.email ?? null, - password: userData.password ?? null, - phone: userData.phone ?? null, - teamMemberId: updatedTeam.id, - }, - }); - } else { - user = null; - } } return { teamMember: updatedTeam, user }; @@ -733,7 +952,7 @@ export class ProjectManagementService { tasks.forEach((t) => { const assigneeName = - (t as any).assignee?.name ?? + t.assignee?.name ?? (t.assigneeId ? teamById.get(t.assigneeId)?.name : null) ?? null; @@ -745,7 +964,7 @@ export class ProjectManagementService { status: t.status, assigneeId: t.assigneeId ?? null, assigneeName, - priority: (t as any).priority ?? null, + priority: t.priority ?? null, startDate: t.startDate ?? null, dueDate: t.dueDate ?? null, createdAt: t.createdAt?.toISOString(), diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 21ef56f..8c9f716 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -26,6 +26,7 @@ "motion": "^12.23.24", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-icons": "^5.5.0", "react-markdown": "^10.1.0", "react-quill-new": "^3.6.0", "react-select": "^5.10.2", @@ -478,467 +479,1316 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, - "node_modules/@esbuild/win32-x64": { + "node_modules/@esbuild/aix-ppc64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", - "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", "cpu": [ - "x64" + "ppc64" ], "license": "MIT", "optional": true, "os": [ - "win32" + "aix" ], "engines": { "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "dev": true, + "node_modules/@esbuild/android-arm": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "cpu": [ + "arm" + ], "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/android-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, + "node_modules/@esbuild/android-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=18" } }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "cpu": [ + "arm" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/js": { - "version": "9.39.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz", - "integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==", - "dev": true, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "cpu": [ + "arm64" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": ">=18" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "cpu": [ + "mips64el" + ], "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.10" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "cpu": [ + "ppc64" + ], "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "license": "MIT" + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@gsap/react": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@gsap/react/-/react-2.1.2.tgz", - "integrity": "sha512-JqliybO1837UcgH2hVOM4VO+38APk3ECNrsuSM4MuXp+rbf+/2IG2K1YJiqfTcXQHH7XlA0m3ykniFYstfq0Iw==", - "license": "SEE LICENSE AT https://gsap.com/standard-license", - "peerDependencies": { - "gsap": "^3.12.5", - "react": ">=17" + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/linux-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "cpu": [ + "ia32" + ], "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 8" + "node": ">=18" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 8" + "node": ">=18" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": ">= 8" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.43", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz", - "integrity": "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==", + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz", + "integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@gsap/react": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@gsap/react/-/react-2.1.2.tgz", + "integrity": "sha512-JqliybO1837UcgH2hVOM4VO+38APk3ECNrsuSM4MuXp+rbf+/2IG2K1YJiqfTcXQHH7XlA0m3ykniFYstfq0Iw==", + "license": "SEE LICENSE AT https://gsap.com/standard-license", + "peerDependencies": { + "gsap": "^3.12.5", + "react": ">=17" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.43", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz", + "integrity": "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==", "dev": true, "license": "MIT" }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-gnu": { "version": "4.52.5", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", "cpu": [ - "x64" + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz", + "integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.16" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz", + "integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.16", + "@tailwindcss/oxide-darwin-arm64": "4.1.16", + "@tailwindcss/oxide-darwin-x64": "4.1.16", + "@tailwindcss/oxide-freebsd-x64": "4.1.16", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.16", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.16", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.16", + "@tailwindcss/oxide-linux-x64-musl": "4.1.16", + "@tailwindcss/oxide-wasm32-wasi": "4.1.16", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.16", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.16" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz", + "integrity": "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz", + "integrity": "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz", + "integrity": "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz", + "integrity": "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz", + "integrity": "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz", + "integrity": "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz", + "integrity": "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==", + "cpu": [ + "arm64" ], "license": "MIT", "optional": true, "os": [ - "win32" - ] + "linux" + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", - "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz", + "integrity": "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ - "win32" - ] + "linux" + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@so-ric/colorspace": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", - "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz", + "integrity": "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "color": "^5.0.2", - "text-hex": "1.0.x" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, - "node_modules/@tailwindcss/node": { + "node_modules/@tailwindcss/oxide-wasm32-wasi": { "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz", - "integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz", + "integrity": "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], "license": "MIT", + "optional": true, "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", - "jiti": "^2.6.1", - "lightningcss": "1.30.2", - "magic-string": "^0.30.19", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.16" + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide": { + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz", - "integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz", + "integrity": "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==", + "cpu": [ + "arm64" + ], "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.16", - "@tailwindcss/oxide-darwin-arm64": "4.1.16", - "@tailwindcss/oxide-darwin-x64": "4.1.16", - "@tailwindcss/oxide-freebsd-x64": "4.1.16", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.16", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.16", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.16", - "@tailwindcss/oxide-linux-x64-musl": "4.1.16", - "@tailwindcss/oxide-wasm32-wasi": "4.1.16", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.16", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.16" } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { @@ -2794,6 +3644,20 @@ } } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3407,6 +4271,206 @@ "lightningcss-win32-x64-msvc": "1.30.2" } }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lightningcss-win32-x64-msvc": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", @@ -4910,6 +5974,15 @@ "react": "^19.2.0" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6174,20 +7247,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 2e2f3fc..118f967 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "motion": "^12.23.24", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-icons": "^5.5.0", "react-markdown": "^10.1.0", "react-quill-new": "^3.6.0", "react-select": "^5.10.2", diff --git a/frontend/src/App.css b/frontend/src/App.css index 4ef2d01..ec21b04 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -58,4 +58,4 @@ .animate-fadeIn { animation: fadeIn 0.8s ease-out both; -} \ No newline at end of file +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 303e40c..c934627 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -55,6 +55,7 @@ function App() { setAuth({ token: r.token, userId: r.userId, + user: r.user, teamMemberId: r.teamMemberId ?? null, }); diff --git a/frontend/src/api/authApi.ts b/frontend/src/api/authApi.ts index d81f633..0befaa4 100644 --- a/frontend/src/api/authApi.ts +++ b/frontend/src/api/authApi.ts @@ -22,12 +22,14 @@ function makeError(res: Response, parsed: any) { export type AuthResult = { token: string; userId: string; + user: any | null; teamMemberId?: string | null; clientTempId?: string | null; }; export async function apiRegister(payload: { clientTempId?: string; + workspace: string; email: string; name: string; password?: string; @@ -55,5 +57,6 @@ export async function apiLogin(payload: { }); const parsed = await parseJson(res); if (!res.ok) throw makeError(res, parsed); + console.log(parsed); return parsed; } diff --git a/frontend/src/api/projectApi.ts b/frontend/src/api/projectApi.ts index a91fcce..0ff0d06 100644 --- a/frontend/src/api/projectApi.ts +++ b/frontend/src/api/projectApi.ts @@ -20,10 +20,10 @@ function makeError(res: Response, parsed: any) { } /** - * State / bootstrap + * Workspaces */ -export async function getState() { - const res = await apiFetch(`${BASE}/api/project-management/state`, { +export async function getWorkspaces() { + const res = await apiFetch(`${BASE}/api/project-management/workspaces`, { method: "GET", }); const parsed = await parseJson(res); @@ -31,11 +31,39 @@ export async function getState() { return parsed; } +export async function createWorkspace(payload: any) { + const res = await apiFetch(`${BASE}/api/workspaces`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + const parsed = await parseJson(res); + if (!res.ok) throw makeError(res, parsed); + return parsed; +} + +/** + * State / bootstrap + */ +export async function getState(workspaceId: string) { + const res = await apiFetch( + `${BASE}/api/project-management/state/${workspaceId}`, + { + method: "GET", + } + ); + const parsed = await parseJson(res); + if (!res.ok) throw makeError(res, parsed); + return parsed; +} + /** * Projects */ -export async function getProjects() { - const res = await apiFetch(`${BASE}/api/projects`, { method: "GET" }); +export async function getProjects(workspaceId: string) { + const res = await apiFetch(`${BASE}/api/projects/${workspaceId}`, { + method: "GET", + }); const parsed = await parseJson(res); if (!res.ok) throw makeError(res, parsed); return parsed; @@ -181,8 +209,10 @@ export async function updateComment( /** * Team members */ -export async function getTeam() { - const res = await apiFetch(`${BASE}/api/team`, { method: "GET" }); +export async function getTeam(workspaceId: string) { + const res = await apiFetch(`${BASE}/api/team/${workspaceId}`, { + method: "GET", + }); const parsed = await parseJson(res); if (!res.ok) throw makeError(res, parsed); return parsed; diff --git a/frontend/src/components/AiAgent.tsx b/frontend/src/components/AiAgent.tsx index e54d43c..cf5c9f5 100644 --- a/frontend/src/components/AiAgent.tsx +++ b/frontend/src/components/AiAgent.tsx @@ -1,75 +1,75 @@ -import { useState, useEffect } from "react" -import "./AiAgent.css" +import { useState, useEffect } from "react"; +import "./AiAgent.css"; function AiAgent({ setIsShow }: any) { - const messages = [ - "πŸ“ˆ Halo, pantau kontribusi yuk!", - "πŸ’‘ Mau analisa cepat?", - "πŸ” Cari kontributor terbaik?", - "πŸ€– Siap bantu analisa!", - "πŸ” Siapa aja yang berkontribusi?", - "πŸ” Ada repo apa aja ya?", - "πŸ’‘ Butuh insight repo?", - "🧠 Analisis aktif!", - ] + const messages = [ + "πŸ“ˆ Halo, pantau kontribusi yuk!", + "πŸ’‘ Mau analisa cepat?", + "πŸ” Cari kontributor terbaik?", + "πŸ€– Siap bantu analisa!", + "πŸ” Siapa aja yang berkontribusi?", + "πŸ” Ada repo apa aja ya?", + "πŸ’‘ Butuh insight repo?", + "🧠 Analisis aktif!", + ]; - const [currentIndex, setCurrentIndex] = useState(0) - const [displayedText, setDisplayedText] = useState("") - const [wordIndex, setWordIndex] = useState(0) - const [isTyping, setIsTyping] = useState(true) + const [currentIndex, setCurrentIndex] = useState(0); + const [displayedText, setDisplayedText] = useState(""); + const [wordIndex, setWordIndex] = useState(0); + const [isTyping, setIsTyping] = useState(true); - useEffect(() => { - const words = messages[currentIndex].split(" ") + useEffect(() => { + const words = messages[currentIndex].split(" "); - if (wordIndex < words.length) { - const timeout = setTimeout(() => { - setDisplayedText((prev) => - prev ? `${prev} ${words[wordIndex]}` : words[wordIndex] - ) - setWordIndex((prev) => prev + 1) - }, 250) - return () => clearTimeout(timeout) - } else { - setIsTyping(false) - const wait = setTimeout(() => { - setDisplayedText("") - setWordIndex(0) - setCurrentIndex((prev) => (prev + 1) % messages.length) - setIsTyping(true) - }, 5000) - return () => clearTimeout(wait) - } - }, [wordIndex, currentIndex]) + if (wordIndex < words.length) { + const timeout = setTimeout(() => { + setDisplayedText((prev) => + prev ? `${prev} ${words[wordIndex]}` : words[wordIndex] + ); + setWordIndex((prev) => prev + 1); + }, 250); + return () => clearTimeout(timeout); + } else { + setIsTyping(false); + const wait = setTimeout(() => { + setDisplayedText(""); + setWordIndex(0); + setCurrentIndex((prev) => (prev + 1) % messages.length); + setIsTyping(true); + }, 5000); + return () => clearTimeout(wait); + } + }, [wordIndex, currentIndex]); - return ( -
setIsShow(true)} - > - {/* Balon chat di kiri */} -
- {displayedText} - {isTyping && |} + return ( +
setIsShow(true)} + > + {/* Balon chat di kiri */} +
+ {displayedText} + {isTyping && |} - {/* Segitiga balon (ekor) */} -
-
+ {/* Segitiga balon (ekor) */} +
+
- {/* Tombol chatbot di kanan */} - -
- ) + {/* Tombol chatbot di kanan */} + +
+ ); } -export default AiAgent +export default AiAgent; diff --git a/frontend/src/components/Auth/AuthCard.tsx b/frontend/src/components/Auth/AuthCard.tsx index db29114..56f265e 100644 --- a/frontend/src/components/Auth/AuthCard.tsx +++ b/frontend/src/components/Auth/AuthCard.tsx @@ -40,6 +40,7 @@ export default function AuthCard({ onAuthSuccess, initialEmail }: Props) { const [tab, setTab] = useState<"login" | "register">("login"); // common states + const [workspace, setWorkspace] = useState(""); const [email, setEmail] = useState(initialEmail || ""); const [name, setName] = useState(""); const [password, setPassword] = useState(""); @@ -64,8 +65,8 @@ export default function AuthCard({ onAuthSuccess, initialEmail }: Props) { const onRegister = async (e: React.FormEvent) => { e.preventDefault(); - if (!name || !email || !password) { - toast.error("Name, email, and password are required"); + if (!workspace || !name || !email || !password) { + toast.error("Workspace, Name, email, and password are required"); return; } @@ -77,7 +78,13 @@ export default function AuthCard({ onAuthSuccess, initialEmail }: Props) { setLoading(true); const clientTempId = generateClientTempId(); try { - const result = await apiRegister({ clientTempId, email, name, password }); + const result = await apiRegister({ + clientTempId, + workspace, + email, + name, + password, + }); toast.success("Register berhasil"); try { // keep consistent key with the rest of your app @@ -111,6 +118,7 @@ export default function AuthCard({ onAuthSuccess, initialEmail }: Props) { toast.error(err?.message || "Login gagal"); return; } + console.log(result); onAuthSuccess?.(result); } catch (err: any) { console.error(err); @@ -218,6 +226,16 @@ export default function AuthCard({ onAuthSuccess, initialEmail }: Props) { disabled={loading} /> + + setWorkspace(e.target.value)} + placeholder="Workspace Name" + required + disabled={loading} + /> +