Compare commits
7 Commits
fix/125
...
feat/local
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ac89ba8c0 | ||
|
|
a2f7582999 | ||
|
|
8a93951464 | ||
|
|
bf2db74528 | ||
|
|
f564b52159 | ||
|
|
2ef2493bb5 | ||
|
|
f105e951e6 |
@@ -21,7 +21,6 @@ S3_REGION=
|
|||||||
S3_ENDPOINT=
|
S3_ENDPOINT=
|
||||||
S3_ACCESS_KEY_ID=
|
S3_ACCESS_KEY_ID=
|
||||||
S3_SECRET_ACCESS_KEY=
|
S3_SECRET_ACCESS_KEY=
|
||||||
S3_FORCE_PATH_STYLE=
|
|
||||||
NEXT_PUBLIC_STORAGE_URL=
|
NEXT_PUBLIC_STORAGE_URL=
|
||||||
NEXT_PUBLIC_AVATAR_BUCKET_NAME=
|
NEXT_PUBLIC_AVATAR_BUCKET_NAME=
|
||||||
NEXT_PUBLIC_STORAGE_DOMAIN=
|
NEXT_PUBLIC_STORAGE_DOMAIN=
|
||||||
|
|||||||
4
.vscode/launch.json
vendored
@@ -6,7 +6,7 @@
|
|||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "pnpm dev",
|
"command": "pnpm dev",
|
||||||
"cwd": "${workspaceFolder}/apps/web",
|
"cwd": "${workspaceFolder}/apps/nextjs",
|
||||||
"skipFiles": ["<node_internals>/**"],
|
"skipFiles": ["<node_internals>/**"],
|
||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
"sourceMapPathOverrides": {
|
"sourceMapPathOverrides": {
|
||||||
@@ -14,4 +14,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
121
CHANGELOG.md
@@ -1,121 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
## [Unreleased](https://github.com/kanbn/kan/compare/v0.2.4...HEAD)
|
|
||||||
|
|
||||||
## [0.2.4](https://github.com/kanbn/kan/compare/v0.2.3...v0.2.4) - 2025-01-14
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Button to view and update board public URL directly from board page
|
|
||||||
- Redirect from root to login page for self-hosted instances
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Updated all label router endpoints to return consistent structure
|
|
||||||
- Added verbose error logging to magic link invitation process
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Updated tRPC dependencies and added OpenAPI meta to providers endpoint
|
|
||||||
|
|
||||||
## [0.2.3](https://github.com/kanbn/kan/compare/v0.2.2...v0.2.3) - 2025-07-07
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Mobile support with responsive navigation and layout
|
|
||||||
- Template selection when creating new boards with predefined lists and labels
|
|
||||||
- S3_FORCE_PATH_STYLE configuration for MinIO compatibility
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Improved mobile UI styling and layout containers
|
|
||||||
- Enhanced iOS mobile support with viewport fixes and zoom prevention
|
|
||||||
- Improved card view layout for mobile devices
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Mobile dashboard min-width issues
|
|
||||||
- Dynamic viewport height on mobile devices
|
|
||||||
- Scroll functionality on card pages
|
|
||||||
|
|
||||||
## [0.2.2](https://github.com/kanbn/kan/compare/v0.2.1...v0.2.2) - 2025-06-28
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Localization and multi-language support
|
|
||||||
- URLs are now rendered as clickable links in card descriptions
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Improved email configuration with optional SMTP authentication
|
|
||||||
|
|
||||||
## [0.2.1](https://github.com/kanbn/kan/compare/v0.2.0...v0.2.1) - 2025-06-19
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- PostHog analytics integration
|
|
||||||
- Debug logging for email functionality
|
|
||||||
- Standalone mode support for Next.js
|
|
||||||
- Environment variable for configuring web port
|
|
||||||
- Docker Compose setup with PostgreSQL
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- SMTP authentication is now optional
|
|
||||||
- Added `SMTP_SECURE` configuration option
|
|
||||||
- Improved Docker build process with better context handling
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Default output mode for Next.js builds
|
|
||||||
- Container environment variable configuration
|
|
||||||
- Docker Compose volume naming
|
|
||||||
- Build context and static file copying in Docker
|
|
||||||
- Comment rendering inside contenteditable elements
|
|
||||||
|
|
||||||
## [0.2.0](https://github.com/kanbn/kan/compare/v0.1.0...v0.2.0) - 2025-06-10
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Trello integration with OAuth authentication and user field mapping
|
|
||||||
- Board visibility controls with dropdown menu and success notifications
|
|
||||||
- Markdown editor for card descriptions
|
|
||||||
- Email/password authentication for self-hosters
|
|
||||||
- Account deletion functionality with cascade delete for workspaces
|
|
||||||
- Enhanced scrollbar styles for dark mode
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Simplified auth configuration to use `NEXT_PUBLIC_BASE_URL` instead of `BETTER_AUTH_URL`
|
|
||||||
- Updated self-hosting documentation
|
|
||||||
- Improved database schema with relation names
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- List buttons stacking issue
|
|
||||||
- Editor onChange prop is now optional
|
|
||||||
- Renamed markdown prop to content in Editor component
|
|
||||||
- Router navigation for disabled signup redirects
|
|
||||||
|
|
||||||
## [0.1.0](https://github.com/kanbn/kan/releases/tag/v0.1.0) - 2025-06-02
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Initial release of Kan
|
|
||||||
- Core project management features
|
|
||||||
- Board, list, and card management
|
|
||||||
- User authentication system
|
|
||||||
- Workspace collaboration
|
|
||||||
- Comment system
|
|
||||||
- Activity logging
|
|
||||||
- Docker deployment support
|
|
||||||
- Self-hosting capabilities
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Updated license link in footer
|
|
||||||
@@ -162,7 +162,6 @@ pnpm dev
|
|||||||
| `S3_ENDPOINT` | S3 endpoint URL | For file uploads | `https://xxx.r2.cloudflarestorage.com` |
|
| `S3_ENDPOINT` | S3 endpoint URL | For file uploads | `https://xxx.r2.cloudflarestorage.com` |
|
||||||
| `S3_ACCESS_KEY_ID` | S3 access key | For file uploads | `xxx` |
|
| `S3_ACCESS_KEY_ID` | S3 access key | For file uploads | `xxx` |
|
||||||
| `S3_SECRET_ACCESS_KEY` | S3 secret key | For file uploads | `xxx` |
|
| `S3_SECRET_ACCESS_KEY` | S3 secret key | For file uploads | `xxx` |
|
||||||
| `S3_FORCE_PATH_STYLE` | Use path-style URLs for S3 | For file uploads | `true` |
|
|
||||||
| `NEXT_PUBLIC_STORAGE_URL` | Storage service URL | For file uploads | `https://storage.kanbn.com` |
|
| `NEXT_PUBLIC_STORAGE_URL` | Storage service URL | For file uploads | `https://storage.kanbn.com` |
|
||||||
| `NEXT_PUBLIC_STORAGE_DOMAIN` | Storage domain name | For file uploads | `kanbn.com` |
|
| `NEXT_PUBLIC_STORAGE_DOMAIN` | Storage domain name | For file uploads | `kanbn.com` |
|
||||||
| `NEXT_PUBLIC_AVATAR_BUCKET_NAME` | S3 bucket name for avatars | For file uploads | `avatars` |
|
| `NEXT_PUBLIC_AVATAR_BUCKET_NAME` | S3 bucket name for avatars | For file uploads | `avatars` |
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 400 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 433 KiB |
@@ -6,6 +6,7 @@ checksums:
|
|||||||
"%7B0%7D%20%7C%20%7B1%7D/singular": 4628f9c6ec13c2dc2984a6ab80e19f8b
|
"%7B0%7D%20%7C%20%7B1%7D/singular": 4628f9c6ec13c2dc2984a6ab80e19f8b
|
||||||
"%7B0%7D%20labels/singular": 785f92bedf786ff3e285adfc07c69d18
|
"%7B0%7D%20labels/singular": 785f92bedf786ff3e285adfc07c69d18
|
||||||
"%7BboardCount%2C%20plural%2C%20one%20%7BImport%20board%20(1)%7D%20other%20%7BImport%20boards%20(%7BboardCount%7D)%7D%7D/singular": 72358c550bd1a99fcb6e8952f0f947ef
|
"%7BboardCount%2C%20plural%2C%20one%20%7BImport%20board%20(1)%7D%20other%20%7BImport%20boards%20(%7BboardCount%7D)%7D%7D/singular": 72358c550bd1a99fcb6e8952f0f947ef
|
||||||
|
"%231%20Hacker%20News/singular": de271b412c6c022d5c427d29fc70e0ba
|
||||||
1%20user/singular: 3b547431ab12f0fba84307e6a81109d8
|
1%20user/singular: 3b547431ab12f0fba84307e6a81109d8
|
||||||
A%20powerful%2C%20flexible%20kanban%20app%20that%20helps%20you%20organise%20work%2C%20track%20progress%2C%20and%20deliver%20results%E2%80%94all%20in%20one%20place./singular: d405b83b0d631cb72f4347c10bcbb643
|
A%20powerful%2C%20flexible%20kanban%20app%20that%20helps%20you%20organise%20work%2C%20track%20progress%2C%20and%20deliver%20results%E2%80%94all%20in%20one%20place./singular: d405b83b0d631cb72f4347c10bcbb643
|
||||||
Account%20deleted/singular: a25da96a1579c4491be0a95669ef18a4
|
Account%20deleted/singular: a25da96a1579c4491be0a95669ef18a4
|
||||||
@@ -26,23 +27,15 @@ checksums:
|
|||||||
An%20error%20occurred%20while%20disconnecting%20your%20Trello%20account./singular: 0aa3973b860c1faf8d9123aebf567e40
|
An%20error%20occurred%20while%20disconnecting%20your%20Trello%20account./singular: 0aa3973b860c1faf8d9123aebf567e40
|
||||||
API%20keys/singular: 07f3620a30e08136f0b072c9af8d1eef
|
API%20keys/singular: 07f3620a30e08136f0b072c9af8d1eef
|
||||||
API%20Reference/singular: 7dcc877064bfdf889ec55dbc9e06a242
|
API%20Reference/singular: 7dcc877064bfdf889ec55dbc9e06a242
|
||||||
Applicants/singular: 4babd8331a1441e91087f855f43e57f8
|
|
||||||
Approval/singular: 2f5426445800ecd5a3102aac940d459c
|
|
||||||
Are%20you%20sure%20want%20to%20remove%20%7BentityLabel%7D%3F/singular: f9b67026c5ef86ac4d5944380d571961
|
Are%20you%20sure%20want%20to%20remove%20%7BentityLabel%7D%3F/singular: f9b67026c5ef86ac4d5944380d571961
|
||||||
Are%20you%20sure%20you%20want%20to%20delete%20the%20workspace%20%7B0%7D%3F/singular: 27ba2854202dd77c51e78c64e5c93946
|
Are%20you%20sure%20you%20want%20to%20delete%20the%20workspace%20%7B0%7D%3F/singular: 27ba2854202dd77c51e78c64e5c93946
|
||||||
Are%20you%20sure%20you%20want%20to%20delete%20this%20card%3F/singular: 131a5a7cccca7be8b32c987d9fa0b0c0
|
Are%20you%20sure%20you%20want%20to%20delete%20this%20card%3F/singular: 131a5a7cccca7be8b32c987d9fa0b0c0
|
||||||
Are%20you%20sure%20you%20want%20to%20delete%20this%20comment%3F/singular: 55fe6f1b52b71b91b2a079cfda3cc7fe
|
Are%20you%20sure%20you%20want%20to%20delete%20this%20comment%3F/singular: 55fe6f1b52b71b91b2a079cfda3cc7fe
|
||||||
Are%20you%20sure%20you%20want%20to%20delete%20your%20account%3F/singular: 428b74d781063224c3e63980e1094ec3
|
Are%20you%20sure%20you%20want%20to%20delete%20your%20account%3F/singular: 428b74d781063224c3e63980e1094ec3
|
||||||
assigned%20%3C0%3E%7BmemberName%7D%3C%2F0%3E%20to%20the%20card/singular: 4fb1c419a78a0f010a3e5790801551fc
|
assigned%20%3C0%3E%7BmemberName%7D%3C%2F0%3E%20to%20the%20card/singular: 4fb1c419a78a0f010a3e5790801551fc
|
||||||
Awaiting%20Customer/singular: 122c108794f321c10e05a08f09b11457
|
|
||||||
Backlog/singular: 35919fd5b10ad212839a75e72829fe4e
|
|
||||||
Basic%20Kanban/singular: e46ebc406749486f3907436970d4d6c7
|
|
||||||
Billing/singular: b01dbdd049ebbd4a349fa64d6ce65a3b
|
Billing/singular: b01dbdd049ebbd4a349fa64d6ce65a3b
|
||||||
Billing%20portal/singular: dc3afa14ffe19f5920992a0c9d49d525
|
Billing%20portal/singular: dc3afa14ffe19f5920992a0c9d49d525
|
||||||
Blog%20Post/singular: 38e027f212be445dc530e427fc642a20
|
|
||||||
Board/singular: 101ff39aab674c033c15ab978d0d8bac
|
Board/singular: 101ff39aab674c033c15ab978d0d8bac
|
||||||
Board%20name%20cannot%20exceed%20100%20characters/singular: 985c0177744ec56d8dac84f66ec284ca
|
|
||||||
Board%20name%20is%20required/singular: dbd0c6cc945ab19e702fc27f970a023b
|
|
||||||
Board%20not%20found/singular: 5af9844595fa8aed5985066620435515
|
Board%20not%20found/singular: 5af9844595fa8aed5985066620435515
|
||||||
Board%20URL%20can%20only%20contain%20letters%2C%20numbers%2C%20and%20hyphens/singular: ba018946ffdf85e55d701a9c27595b4d
|
Board%20URL%20can%20only%20contain%20letters%2C%20numbers%2C%20and%20hyphens/singular: ba018946ffdf85e55d701a9c27595b4d
|
||||||
Board%20URL%20cannot%20exceed%2060%20characters/singular: 8fd3417559a56aa42d297b14d480c382
|
Board%20URL%20cannot%20exceed%2060%20characters/singular: 8fd3417559a56aa42d297b14d480c382
|
||||||
@@ -52,9 +45,6 @@ checksums:
|
|||||||
Board%20visibility%20updated/singular: 8e2f5d622fa9c94481e7c1629a154ecf
|
Board%20visibility%20updated/singular: 8e2f5d622fa9c94481e7c1629a154ecf
|
||||||
Boards/singular: 8fff51c5872e051beb9b718336665e80
|
Boards/singular: 8fff51c5872e051beb9b718336665e80
|
||||||
Boards%20%7C%20%7B0%7D/singular: 70c349df806f4b2d8bd28136a45496f0
|
Boards%20%7C%20%7B0%7D/singular: 70c349df806f4b2d8bd28136a45496f0
|
||||||
Brainstorming/singular: 736332f2e4488609e42d2be8547d296e
|
|
||||||
Bug/singular: 4509fffdb5931f8905063c80cf802d71
|
|
||||||
Bug%20Report/singular: e558d1f100e21230c2f495a8913ac5ec
|
|
||||||
Cancel/singular: 2e2a849c2223911717de8caa2c71bade
|
Cancel/singular: 2e2a849c2223911717de8caa2c71bade
|
||||||
Card%20not%20found/singular: 91509e2f92b0b3b11330b6983139fdbf
|
Card%20not%20found/singular: 91509e2f92b0b3b11330b6983139fdbf
|
||||||
Card%20title/singular: 7c34f59f4005e6cb3a6ff546ea0b96e3
|
Card%20title/singular: 7c34f59f4005e6cb3a6ff546ea0b96e3
|
||||||
@@ -62,12 +52,10 @@ checksums:
|
|||||||
Check%20your%20inbox/singular: e9a430fcd298def74212238df0f680d6
|
Check%20your%20inbox/singular: e9a430fcd298def74212238df0f680d6
|
||||||
Clear%20filters/singular: 8f40ab5af527e4b190da94e7b6221379
|
Clear%20filters/singular: 8f40ab5af527e4b190da94e7b6221379
|
||||||
Click%20on%20the%20link%20we've%20sent%20to%20%7BmagicLinkRecipient%7D%20to%20sign%20in./singular: 210b6ff8727f976182ec3f29ea3c7667
|
Click%20on%20the%20link%20we've%20sent%20to%20%7BmagicLinkRecipient%7D%20to%20sign%20in./singular: 210b6ff8727f976182ec3f29ea3c7667
|
||||||
Code%20Review/singular: a2da6c2339301e7c3ddf068c4ff9f7e8
|
|
||||||
Collaborate%20seamlessly%20with%20your%20team./singular: c5f10e431aaf8b51519bc009a4a4080c
|
Collaborate%20seamlessly%20with%20your%20team./singular: c5f10e431aaf8b51519bc009a4a4080c
|
||||||
Coming%20soon/singular: ee2b0671e00972773210c5be5a9ccb89
|
Coming%20soon/singular: ee2b0671e00972773210c5be5a9ccb89
|
||||||
Comments/singular: 502237cd1044491c48494aa60967a985
|
Comments/singular: 502237cd1044491c48494aa60967a985
|
||||||
Company/singular: 9e36d11fa82d1bd4563eef2abc4fcfba
|
Company/singular: 9e36d11fa82d1bd4563eef2abc4fcfba
|
||||||
Complete/singular: d3e36d723de0cfdea51c5b4171d30b4d
|
|
||||||
Complete%20control%20and%20ownership%3A/singular: 0d8b682ba873272217425ccfc96aa9cd
|
Complete%20control%20and%20ownership%3A/singular: 0d8b682ba873272217425ccfc96aa9cd
|
||||||
Confirm%20URL%20change/singular: e14b4ce081d1ef6194b37e1814e97b39
|
Confirm%20URL%20change/singular: e14b4ce081d1ef6194b37e1814e97b39
|
||||||
Connect%20Trello/singular: 4440a0b9e387ef7136e3958e7a089213
|
Connect%20Trello/singular: 4440a0b9e387ef7136e3958e7a089213
|
||||||
@@ -75,7 +63,6 @@ checksums:
|
|||||||
Connect%20your%20Trello%20account%20to%20import%20boards./singular: 84a510a7485f43e43f2191bd161171d7
|
Connect%20your%20Trello%20account%20to%20import%20boards./singular: 84a510a7485f43e43f2191bd161171d7
|
||||||
Contact/singular: 9afa39bc47019ee6dec6c74b6273967c
|
Contact/singular: 9afa39bc47019ee6dec6c74b6273967c
|
||||||
Contact%20us/singular: 694a45bc5c96f6f3a27873c1cf5a7707
|
Contact%20us/singular: 694a45bc5c96f6f3a27873c1cf5a7707
|
||||||
Content%20Creation/singular: 4e6be22ade0188f408471325fe363575
|
|
||||||
Continue%20with%20/singular: 8ed03cf7c5e60a6edf3470a4558ff058
|
Continue%20with%20/singular: 8ed03cf7c5e60a6edf3470a4558ff058
|
||||||
Continue%20with%20%7B0%7D/singular: 2eaf6e1da91e208f7c5fb6bf862fe8a6
|
Continue%20with%20%7B0%7D/singular: 2eaf6e1da91e208f7c5fb6bf862fe8a6
|
||||||
Control%20who%20can%20view%20and%20edit%20your%20boards./singular: 2a7e0bec29bac26280de707e2fe8bce5
|
Control%20who%20can%20view%20and%20edit%20your%20boards./singular: 2a7e0bec29bac26280de707e2fe8bce5
|
||||||
@@ -90,10 +77,8 @@ checksums:
|
|||||||
Create%20new%20list/singular: ce61ca8be7b3f6c26c32b684a42a2c26
|
Create%20new%20list/singular: ce61ca8be7b3f6c26c32b684a42a2c26
|
||||||
Create%20workspace/singular: 2e6718e79964ea5ce22d76c2189c77ca
|
Create%20workspace/singular: 2e6718e79964ea5ce22d76c2189c77ca
|
||||||
created%20the%20card/singular: 605475f5aaeb4dbccbf7c4eb9107b43d
|
created%20the%20card/singular: 605475f5aaeb4dbccbf7c4eb9107b43d
|
||||||
Critical/singular: eb327cd411b50aee954f8d1d215d003a
|
|
||||||
Custom%20domain/singular: b09e7a9c187b7163b4a6cfc78042fe42
|
Custom%20domain/singular: b09e7a9c187b7163b4a6cfc78042fe42
|
||||||
Custom%20URLs%20are%20a%20premium%20feature.%20You'll%20be%20directed%20to%20upgrade%20your%20account./singular: b52e4f6122cf739c9551d29beb89b25e
|
Custom%20URLs%20are%20a%20premium%20feature.%20You'll%20be%20directed%20to%20upgrade%20your%20account./singular: b52e4f6122cf739c9551d29beb89b25e
|
||||||
Customer%20Support/singular: 50e3c77e22e41061ca85ea2f02625a2e
|
|
||||||
Dark/singular: 73e6e208ba628b26e90fcf6dce15e1b2
|
Dark/singular: 73e6e208ba628b26e90fcf6dce15e1b2
|
||||||
Delete/singular: 8bcf303dd10a645b5baacb02b47d72c9
|
Delete/singular: 8bcf303dd10a645b5baacb02b47d72c9
|
||||||
Delete%20account/singular: a9d11113f1a1d7e20582bdcc9633bcef
|
Delete%20account/singular: a9d11113f1a1d7e20582bdcc9633bcef
|
||||||
@@ -102,7 +87,6 @@ checksums:
|
|||||||
Delete%20comment/singular: a3dae0c9ddbc73677aad9812e3dffd69
|
Delete%20comment/singular: a3dae0c9ddbc73677aad9812e3dffd69
|
||||||
Delete%20list/singular: b694f58e4c973d8797a78e746b550b1f
|
Delete%20list/singular: b694f58e4c973d8797a78e746b550b1f
|
||||||
Delete%20workspace/singular: 9017c821209528d9b20d40a488854cdf
|
Delete%20workspace/singular: 9017c821209528d9b20d40a488854cdf
|
||||||
Design/singular: 991b75727b6784c1a063a7462b76186d
|
|
||||||
Disconnect%20Trello/singular: a2dd737d133887d34504728c7e7f05e3
|
Disconnect%20Trello/singular: a2dd737d133887d34504728c7e7f05e3
|
||||||
Discuss%20and%20collaborate%20on%20cards./singular: f2e87816709f602130831ead50bba457
|
Discuss%20and%20collaborate%20on%20cards./singular: f2e87816709f602130831ead50bba457
|
||||||
Display%20name/singular: 725f331065e9d594d720d07b1ae51a90
|
Display%20name/singular: 725f331065e9d594d720d07b1ae51a90
|
||||||
@@ -114,15 +98,11 @@ checksums:
|
|||||||
Docs/singular: 55fea190a0c0fb32acd3dc986cd3cc91
|
Docs/singular: 55fea190a0c0fb32acd3dc986cd3cc91
|
||||||
Documentation/singular: 1563fcb5ddb5037b0709ccd3dd384a92
|
Documentation/singular: 1563fcb5ddb5037b0709ccd3dd384a92
|
||||||
Don't%20have%20an%20account%3F%20%3C0%3E%3C1%3ESign%20up%3C%2F1%3E%3C%2F0%3E/singular: 6ae8282ab8b4a01b4ce87b119e13714b
|
Don't%20have%20an%20account%3F%20%3C0%3E%3C1%3ESign%20up%3C%2F1%3E%3C%2F0%3E/singular: 6ae8282ab8b4a01b4ce87b119e13714b
|
||||||
Done/singular: ffd408fa29d5bc9039ef8ea1b9b699bb
|
|
||||||
Edit%20board%20URL/singular: d8276dfc0189f371ec7d80a2047c07aa
|
Edit%20board%20URL/singular: d8276dfc0189f371ec7d80a2047c07aa
|
||||||
Edit%20comment/singular: 7e4b46525fcb6b47b71798e31c46e374
|
Edit%20comment/singular: 7e4b46525fcb6b47b71798e31c46e374
|
||||||
Edit%20label/singular: 0309e0be1512b1e0b0ceb87c69a53d03
|
Edit%20label/singular: 0309e0be1512b1e0b0ceb87c69a53d03
|
||||||
Edit%20workspace%20URL/singular: bbae5f2f8a442947d33099979bbbe899
|
|
||||||
Editing/singular: 3449a7988cd69207b7c6929af1f4abf1
|
|
||||||
email/singular: f31eb214738e037d58e26149797739df
|
email/singular: f31eb214738e037d58e26149797739df
|
||||||
Email/singular: e7f34943a0c2fb849db1839ff6ef5cb5
|
Email/singular: e7f34943a0c2fb849db1839ff6ef5cb5
|
||||||
Enhancement/singular: 785fe23c0eef0a5b60b5b2a88151de31
|
|
||||||
Enter%20your%20email%20address/singular: 9bc008365ebe3e404e241c8ca876f56e
|
Enter%20your%20email%20address/singular: 9bc008365ebe3e404e241c8ca876f56e
|
||||||
Enter%20your%20name/singular: cd95fbdd0533f2c2e8edf9d9bd9aa8df
|
Enter%20your%20name/singular: cd95fbdd0533f2c2e8edf9d9bd9aa8df
|
||||||
Enter%20your%20password/singular: ea4fdd034522dead21bae0c0abb52eae
|
Enter%20your%20password/singular: ea4fdd034522dead21bae0c0abb52eae
|
||||||
@@ -137,11 +117,8 @@ checksums:
|
|||||||
Error%20uploading%20profile%20image/singular: 0ba02753a797a74c6ea7b622c9f69c71
|
Error%20uploading%20profile%20image/singular: 0ba02753a797a74c6ea7b622c9f69c71
|
||||||
Everything%20in%20the%20free%20plan%2C%20plus%3A/singular: 62b44c4973b92b806c69a4b15e0256dc
|
Everything%20in%20the%20free%20plan%2C%20plus%3A/singular: 62b44c4973b92b806c69a4b15e0256dc
|
||||||
Everything%20you%20need%2C%20free%20forever.%20Unlimited%20boards%2C%20unlimited%20lists%2C%20unlimited%20cards.%20Upgrade%20any%20time./singular: fa21632ab1468edf10acda2fe7b71323
|
Everything%20you%20need%2C%20free%20forever.%20Unlimited%20boards%2C%20unlimited%20lists%2C%20unlimited%20cards.%20Upgrade%20any%20time./singular: fa21632ab1468edf10acda2fe7b71323
|
||||||
Execution/singular: cbac4a3c721123cbc6a883560bf29800
|
|
||||||
Failed%20to%20login%20with%20%7B0%7D.%20Please%20try%20again./singular: 669a4b4247a73f53fb9b8b16e42d166f
|
Failed%20to%20login%20with%20%7B0%7D.%20Please%20try%20again./singular: 669a4b4247a73f53fb9b8b16e42d166f
|
||||||
FAQs/singular: dc36d7992372ccd419b39ef51cf5b16c
|
FAQs/singular: dc36d7992372ccd419b39ef51cf5b16c
|
||||||
Feature/singular: 58f5f3f37862b6312a2f20ec1a1fd0e8
|
|
||||||
Feature%20Request/singular: c9de91b9a7020213381977f7d147064f
|
|
||||||
Features/singular: 341ff316a339b106a178f0b8d362951b
|
Features/singular: 341ff316a339b106a178f0b8d362951b
|
||||||
Feedback/singular: 6fac88806e0c269a30777b283988c61c
|
Feedback/singular: 6fac88806e0c269a30777b283988c61c
|
||||||
Feedback%20sent/singular: 38a490c7ad5f1aa6013dfa879d2c7818
|
Feedback%20sent/singular: 38a490c7ad5f1aa6013dfa879d2c7818
|
||||||
@@ -150,8 +127,6 @@ checksums:
|
|||||||
For%20long-term%20sustainability%2C%20we%20recognise%20all%20good%20open%20source%20projects%20need%20a%20source%20of%20revenue.%20Ours%20comes%20from%20the%20paid%20cloud%20offering%20for%20workspaces%20with%20multiple%20users%20(free%20during%20the%20beta)%20and%20for%20custom%20workspace%20slugs.%20There's%20no%20obligation%20to%20use%20it%2C%20and%20you%20can%20self-host%20the%20software%20on%20your%20own%20infrastructure%20free%20of%20charge./singular: 4eedacd4311ff69bf0fd87083c90a268
|
For%20long-term%20sustainability%2C%20we%20recognise%20all%20good%20open%20source%20projects%20need%20a%20source%20of%20revenue.%20Ours%20comes%20from%20the%20paid%20cloud%20offering%20for%20workspaces%20with%20multiple%20users%20(free%20during%20the%20beta)%20and%20for%20custom%20workspace%20slugs.%20There's%20no%20obligation%20to%20use%20it%2C%20and%20you%20can%20self-host%20the%20software%20on%20your%20own%20infrastructure%20free%20of%20charge./singular: 4eedacd4311ff69bf0fd87083c90a268
|
||||||
Free/singular: 0326365539c004f6088656f692602078
|
Free/singular: 0326365539c004f6088656f692602078
|
||||||
Free%2C%20forever/singular: ae4f2f81e88a50b5f250ef8e8e77e74b
|
Free%2C%20forever/singular: ae4f2f81e88a50b5f250ef8e8e77e74b
|
||||||
Full-time/singular: fe6e201f6676ae354111435cf6c76910
|
|
||||||
Fun/singular: 395bdc48e943762b6cde649f4a96771d
|
|
||||||
Get%20started/singular: 5c783951b0100a168bdd2161ff294833
|
Get%20started/singular: 5c783951b0100a168bdd2161ff294833
|
||||||
Get%20Started/singular: 1d5f030c4ec9c869e647ae060518b948
|
Get%20Started/singular: 1d5f030c4ec9c869e647ae060518b948
|
||||||
Get%20started%20by%20creating%20a%20new%20board/singular: 8fe63ba47e12bbf950f3eb33d1008993
|
Get%20started%20by%20creating%20a%20new%20board/singular: 8fe63ba47e12bbf950f3eb33d1008993
|
||||||
@@ -162,8 +137,6 @@ checksums:
|
|||||||
Getting%20started/singular: 8e5e7bd026b5bec46bbfdce02ab9e0b8
|
Getting%20started/singular: 8e5e7bd026b5bec46bbfdce02ab9e0b8
|
||||||
GitHub/singular: 6e1cf3c00fa6fbe24afcc78ea3b5f3e4
|
GitHub/singular: 6e1cf3c00fa6fbe24afcc78ea3b5f3e4
|
||||||
Go%20to%20app/singular: 896d0441384dcd2bfb3b23d61ff1944d
|
Go%20to%20app/singular: 896d0441384dcd2bfb3b23d61ff1944d
|
||||||
High%20Priority/singular: 5d231ff8254aabc875f194c4b4f49c97
|
|
||||||
Hired/singular: e5a9b1bd409b007141fe3d7890022f9a
|
|
||||||
Host%20Kan%20on%20your%20own%20infrastructure.%20Ideal%20for%20organisations%20that%20need%20complete%20control%20over%20their%20data./singular: 8e7ae0783d60ef4624d3caf9bfc3747f
|
Host%20Kan%20on%20your%20own%20infrastructure.%20Ideal%20for%20organisations%20that%20need%20complete%20control%20over%20their%20data./singular: 8e7ae0783d60ef4624d3caf9bfc3747f
|
||||||
How%20do%20I%20get%20a%20custom%20URL%3F/singular: 5b28fb95969c0dd19e1b9c9ab8a5fb29
|
How%20do%20I%20get%20a%20custom%20URL%3F/singular: 5b28fb95969c0dd19e1b9c9ab8a5fb29
|
||||||
How%20do%20I%20import%20my%20Trello%20boards%3F/singular: 44b30a727150cb93364af0a10346012a
|
How%20do%20I%20import%20my%20Trello%20boards%3F/singular: 44b30a727150cb93364af0a10346012a
|
||||||
@@ -172,29 +145,23 @@ checksums:
|
|||||||
How%20is%20the%20project%20funded%3F/singular: ba7e936f7b1b5faa47df59a0ef990092
|
How%20is%20the%20project%20funded%3F/singular: ba7e936f7b1b5faa47df59a0ef990092
|
||||||
I%20acknowledge%20that%20all%20of%20my%20account%20data%20will%20be%20permanently%20deleted%20and%20want%20to%20proceed./singular: 2ffc824d0b1f9e883c2e87ed4b8b40b6
|
I%20acknowledge%20that%20all%20of%20my%20account%20data%20will%20be%20permanently%20deleted%20and%20want%20to%20proceed./singular: 2ffc824d0b1f9e883c2e87ed4b8b40b6
|
||||||
I%20acknowledge%20that%20all%20of%20the%20workspace%20data%20will%20be%20permanently%20deleted%20and%20want%20to%20proceed./singular: fe1183a7439299210d8ab7379b8f4d01
|
I%20acknowledge%20that%20all%20of%20the%20workspace%20data%20will%20be%20permanently%20deleted%20and%20want%20to%20proceed./singular: fe1183a7439299210d8ab7379b8f4d01
|
||||||
Ideas/singular: 27c24b56895defecd10b0171b39bc93f
|
|
||||||
Ideas%20to%20improve%20this%20page.../singular: 285decc2cc28403fae043d9b4de97a00
|
Ideas%20to%20improve%20this%20page.../singular: 285decc2cc28403fae043d9b4de97a00
|
||||||
Import/singular: 348b8ab981de5b7f1fca6d7302263bbd
|
Import/singular: 348b8ab981de5b7f1fca6d7302263bbd
|
||||||
Import%20complete/singular: 7e749df6f068b9cdf1055ea38c9a80af
|
Import%20complete/singular: 7e749df6f068b9cdf1055ea38c9a80af
|
||||||
Import%20failed/singular: bcec1f271c82ddffc91ebc22021b03cb
|
Import%20failed/singular: bcec1f271c82ddffc91ebc22021b03cb
|
||||||
Import%20your%20Trello%20boards%20and%20hit%20the%20ground%20running./singular: d66d13649f81342f8b3a4f3b54370c20
|
Import%20your%20Trello%20boards%20and%20hit%20the%20ground%20running./singular: d66d13649f81342f8b3a4f3b54370c20
|
||||||
Important/singular: 4cf0e8fc8e4e7c5c9b9458059a172a2b
|
|
||||||
Importing%20from%20Trello/singular: 1e3a46fb9bd8ce8c7d632f9555b5bd7c
|
Importing%20from%20Trello/singular: 1e3a46fb9bd8ce8c7d632f9555b5bd7c
|
||||||
Importing%20your%20Trello%20boards%20into%20Kan%20is%20easy.%20You%20can%20follow%20our%20step-by-step%20guide%20%3C0%3Ehere%3C%2F0%3E./singular: ca88dc22801fef08fee1ecfb1bf6914e
|
Importing%20your%20Trello%20boards%20into%20Kan%20is%20easy.%20You%20can%20follow%20our%20step-by-step%20guide%20%3C0%3Ehere%3C%2F0%3E./singular: ca88dc22801fef08fee1ecfb1bf6914e
|
||||||
In%20Progress/singular: 3de9afebcb9d4ce8ac42e14995f79ffd
|
|
||||||
Individuals/singular: 83bfc81f20e60556c44ec80c7d0603ec
|
Individuals/singular: 83bfc81f20e60556c44ec80c7d0603ec
|
||||||
Integrations/singular: 0ccce343287704cd90150c32e2fcad36
|
Integrations/singular: 0ccce343287704cd90150c32e2fcad36
|
||||||
Interviewing/singular: 4ccdcdc784547e925077c3297bddee95
|
|
||||||
Invite/singular: 181884cea804cbde665f160811ee7ad0
|
Invite/singular: 181884cea804cbde665f160811ee7ad0
|
||||||
Invite%20member/singular: ade922db1be6b26bc979565ce5de2bc7
|
Invite%20member/singular: ade922db1be6b26bc979565ce5de2bc7
|
||||||
Junior/singular: ed1bd2c59a824fdcdd56fc8a0660fe9f
|
|
||||||
Kanban%20is%20better%20with%20a%20team.%20Perfect%20for%20small%20and%20growing%20teams%20looking%20to%20collaborate./singular: a77bee43046b260797c8936ad23e9223
|
Kanban%20is%20better%20with%20a%20team.%20Perfect%20for%20small%20and%20growing%20teams%20looking%20to%20collaborate./singular: a77bee43046b260797c8936ad23e9223
|
||||||
Kanban%20reimagined/singular: 613ccfdd9f54c66cbf68cfa313498766
|
Kanban%20reimagined/singular: 613ccfdd9f54c66cbf68cfa313498766
|
||||||
Keep%20in%20mind%20that%20this%20action%20is%20irreversible./singular: 79702b2ad3be71cb8b87f1122a60c199
|
Keep%20in%20mind%20that%20this%20action%20is%20irreversible./singular: 79702b2ad3be71cb8b87f1122a60c199
|
||||||
Labels/singular: 6f15627a90002323eac018274b6922d6
|
Labels/singular: 6f15627a90002323eac018274b6922d6
|
||||||
Labels%20%26%20Filters/singular: 8f0bdd6084516f8241cb613178114390
|
Labels%20%26%20Filters/singular: 8f0bdd6084516f8241cb613178114390
|
||||||
Language/singular: 277fd1a41cc237a437cd1d5e4a80463b
|
Language/singular: 277fd1a41cc237a437cd1d5e4a80463b
|
||||||
Learning/singular: 90e91da3f870f065f2da9aa9adf53514
|
|
||||||
Legal/singular: 00d3f08da61a21887009819cf93fb414
|
Legal/singular: 00d3f08da61a21887009819cf93fb414
|
||||||
License/singular: ac2bcac75151e2fcd9eb768cea275a77
|
License/singular: ac2bcac75151e2fcd9eb768cea275a77
|
||||||
Light/singular: bb78ea2c6edd8662676c81a8a13ecb93
|
Light/singular: bb78ea2c6edd8662676c81a8a13ecb93
|
||||||
@@ -203,10 +170,7 @@ checksums:
|
|||||||
List%20name/singular: e925e2e6ccaf0eb4064a888aaea8d3c2
|
List%20name/singular: e925e2e6ccaf0eb4064a888aaea8d3c2
|
||||||
Login%20%7C%20kan.bn/singular: 42a6c8dcd73e0d46e652646dc86871eb
|
Login%20%7C%20kan.bn/singular: 42a6c8dcd73e0d46e652646dc86871eb
|
||||||
Logout/singular: 07948fdf20705e04a7bf68ab197512bf
|
Logout/singular: 07948fdf20705e04a7bf68ab197512bf
|
||||||
Long-term/singular: 51cb6f0b112250c5a091ca5f0866904b
|
|
||||||
Low%20Priority/singular: 509b5cb7c1e8a185e0aa6d38c64c90e8
|
|
||||||
magic%20link/singular: db9d17a5bd98bba8aa0d8068c68579f3
|
magic%20link/singular: db9d17a5bd98bba8aa0d8068c68579f3
|
||||||
Medium%20Priority/singular: 1f527cd6d1ed602930bcaa303f503b51
|
|
||||||
Members/singular: 0932e80cba1e3e0a7f52bb67ff31da32
|
Members/singular: 0932e80cba1e3e0a7f52bb67ff31da32
|
||||||
Members%20%7C%20%7B0%7D/singular: a29e3e9f1076acd178c417d047584e88
|
Members%20%7C%20%7B0%7D/singular: a29e3e9f1076acd178c417d047584e88
|
||||||
Monthly/singular: 818f1192e32bb855597f930d3e78806e
|
Monthly/singular: 818f1192e32bb855597f930d3e78806e
|
||||||
@@ -220,27 +184,19 @@ checksums:
|
|||||||
New%20import/singular: e8fddfe15dd170277df919e5d37efd47
|
New%20import/singular: e8fddfe15dd170277df919e5d37efd47
|
||||||
New%20label/singular: 890040da52a88c16c249a4ac78980f81
|
New%20label/singular: 890040da52a88c16c249a4ac78980f81
|
||||||
New%20list/singular: 146c2e642b11056a0b07ed65733e4178
|
New%20list/singular: 146c2e642b11056a0b07ed65733e4178
|
||||||
New%20Ticket/singular: 665ff16ec736cba20e8a1b3beb3baa09
|
|
||||||
New%20workspace/singular: 8eecf05b4ce29aa880adfed2e11476af
|
New%20workspace/singular: 8eecf05b4ce29aa880adfed2e11476af
|
||||||
Newsletter/singular: dcfb9666301924431ac8ae88e5b6f9d7
|
|
||||||
Next%20Steps/singular: 12392fe73ae60b6e5a9d851256b8477e
|
|
||||||
No%20boards/singular: e4397619e69b105e4317dda4560ff4a6
|
No%20boards/singular: e4397619e69b105e4317dda4560ff4a6
|
||||||
No%20boards%20found/singular: e044088d2c51b6bdf660fafa86ee1e17
|
No%20boards%20found/singular: e044088d2c51b6bdf660fafa86ee1e17
|
||||||
No%20credit%20card%20required/singular: 2090aa4171dc0b60735069f89b592883
|
No%20credit%20card%20required/singular: 2090aa4171dc0b60735069f89b592883
|
||||||
No%20lists/singular: cedf633d99c77ff4356e089f2d98c0a6
|
No%20lists/singular: cedf633d99c77ff4356e089f2d98c0a6
|
||||||
Offer/singular: 82b4e0c9a3f5b4bd93590847de7c32a1
|
|
||||||
Onboarding/singular: 52b23f9c62ff199d4c09920e7641829e
|
|
||||||
Once%20you%20delete%20your%20account%2C%20there%20is%20no%20going%20back.%20This%20action%20cannot%20be%20undone./singular: 9cf7aa6ef30890e5124e266c081bae1c
|
Once%20you%20delete%20your%20account%2C%20there%20is%20no%20going%20back.%20This%20action%20cannot%20be%20undone./singular: 9cf7aa6ef30890e5124e266c081bae1c
|
||||||
Once%20you%20delete%20your%20workspace%2C%20there%20is%20no%20going%20back.%20This%20action%20cannot%20be%20undone./singular: c9ce6a516ed1f361653e8f8db3dd5ffd
|
Once%20you%20delete%20your%20workspace%2C%20there%20is%20no%20going%20back.%20This%20action%20cannot%20be%20undone./singular: c9ce6a516ed1f361653e8f8db3dd5ffd
|
||||||
or/singular: 7b133c38bec0d5ee23cc6bcf9a8de50b
|
or/singular: 7b133c38bec0d5ee23cc6bcf9a8de50b
|
||||||
Organize%20and%20find%20cards%20quickly%20with%20powerful%20filtering%20tools./singular: 1b9898c4b21e9dff413b4f76dc59db56
|
Organize%20and%20find%20cards%20quickly%20with%20powerful%20filtering%20tools./singular: 1b9898c4b21e9dff413b4f76dc59db56
|
||||||
Own%20your%20data/singular: cc2178dac4bdf6b07f030cfc2a7510e6
|
Own%20your%20data/singular: cc2178dac4bdf6b07f030cfc2a7510e6
|
||||||
Part-time/singular: 213d63da450f35dabb3ab0e35e29feed
|
|
||||||
Payment%20frequency/singular: 63ded0e4ffb462ca8bd33d38e4691d86
|
Payment%20frequency/singular: 63ded0e4ffb462ca8bd33d38e4691d86
|
||||||
Pending/singular: 030a6f3395d5d4efddd3cc67d6009039
|
Pending/singular: 030a6f3395d5d4efddd3cc67d6009039
|
||||||
per%20user%2Fmonth/singular: 72af182c1ba6df6732640f4d8a78d360
|
per%20user%2Fmonth/singular: 72af182c1ba6df6732640f4d8a78d360
|
||||||
Personal%20Project/singular: d7820b1bf4efecc61ed89234567aaa9c
|
|
||||||
Planning/singular: 353f58c75248275fe091740607501610
|
|
||||||
Please%20enter%20a%20valid%20email%20address/singular: 8de4bc8832b11b380bc4cbcedc16e48b
|
Please%20enter%20a%20valid%20email%20address/singular: 8de4bc8832b11b380bc4cbcedc16e48b
|
||||||
Please%20enter%20a%20valid%20name/singular: f2d741f1b5cae722e35cb5206786f932
|
Please%20enter%20a%20valid%20name/singular: f2d741f1b5cae722e35cb5206786f932
|
||||||
Please%20enter%20a%20valid%20password/singular: 4b32c17e19b79bcbf0bb092c06ba310f
|
Please%20enter%20a%20valid%20password/singular: 4b32c17e19b79bcbf0bb092c06ba310f
|
||||||
@@ -253,35 +209,25 @@ checksums:
|
|||||||
Profile%20image%20updated/singular: 1ba6609b50bc217a36ab659563e78dae
|
Profile%20image%20updated/singular: 1ba6609b50bc217a36ab659563e78dae
|
||||||
Profile%20picture/singular: d9b5e595877ffe2557486a987dd28274
|
Profile%20picture/singular: d9b5e595877ffe2557486a987dd28274
|
||||||
Public/singular: c834f11dab0881632aded3f01d4f8fa9
|
Public/singular: c834f11dab0881632aded3f01d4f8fa9
|
||||||
Publishing/singular: d9c8862c0180d9b7bd1272d5dc0d3ac3
|
|
||||||
Question/singular: 0576462ce60d4263d7c482463fcc9547
|
|
||||||
Questions%3F/singular: c7c3a47f8b230ce7a43f3c090f21d683
|
Questions%3F/singular: c7c3a47f8b230ce7a43f3c090f21d683
|
||||||
Quick%20Win/singular: a03ff4c0896f14b87455510bab203511
|
|
||||||
Recruitment/singular: 78084ee4954aa22efe9ad7463d2a0c72
|
|
||||||
Remote/singular: dc3e4280dfe5c455b38ba6c8884999cf
|
|
||||||
Remove/singular: dba2fe5fe9f83f8078c687f28cba4b52
|
Remove/singular: dba2fe5fe9f83f8078c687f28cba4b52
|
||||||
Remove%20member/singular: 1d77c2ca3768e486fd1fb5df30cb60d9
|
Remove%20member/singular: 1d77c2ca3768e486fd1fb5df30cb60d9
|
||||||
removed%20a%20label%20from%20the%20card/singular: f83ec18850a2a8a3f8242a740a04df06
|
removed%20a%20label%20from%20the%20card/singular: f83ec18850a2a8a3f8242a740a04df06
|
||||||
removed%20a%20member%20from%20the%20card/singular: 0bd7561bb79358fde3d595c3e60a315a
|
removed%20a%20member%20from%20the%20card/singular: 0bd7561bb79358fde3d595c3e60a315a
|
||||||
removed%20label%20%3C0%3E%7Blabel%7D%3C%2F0%3E/singular: 1e7ebc1866136352bdc3cd90afc083f0
|
removed%20label%20%3C0%3E%7Blabel%7D%3C%2F0%3E/singular: 1e7ebc1866136352bdc3cd90afc083f0
|
||||||
Research/singular: 3368e9638d1619babd6df9fad592274f
|
|
||||||
Resolution/singular: 6d8bd9e1bd7dae5ae38c93061d32990e
|
|
||||||
Resources/singular: ec7fb05ed963bb6781a35782b3475502
|
Resources/singular: ec7fb05ed963bb6781a35782b3475502
|
||||||
Review/singular: 299f75db25382980b2895622d7712927
|
|
||||||
Revoke/singular: be57685a85b6dfeaeb6eab4e9560b520
|
Revoke/singular: be57685a85b6dfeaeb6eab4e9560b520
|
||||||
Roadmap/singular: c60f4a1acf30e566861bf130f13b9ae7
|
Roadmap/singular: c60f4a1acf30e566861bf130f13b9ae7
|
||||||
Role/singular: 53743bbb6ca938f5b893552e839d067f
|
Role/singular: 53743bbb6ca938f5b893552e839d067f
|
||||||
Run%20on%20your%20own%20infrastructure/singular: eba804911562b8dbf9d69c3e27f1d708
|
Run%20on%20your%20own%20infrastructure/singular: eba804911562b8dbf9d69c3e27f1d708
|
||||||
Save/singular: f7a2929f33bc420195e59ac5a8bcd454
|
Save/singular: f7a2929f33bc420195e59ac5a8bcd454
|
||||||
Save%20time%20with%20reusable%20board%20templates./singular: d0f2d7d0fd682ceaf4ca12c6353fd75a
|
Save%20time%20with%20reusable%20board%20templates./singular: d0f2d7d0fd682ceaf4ca12c6353fd75a
|
||||||
Screening/singular: 0327bfb25db87bdacf00d4dc297f8b26
|
|
||||||
Select%20all/singular: eedc7cdb02de467c15dc418a066a77f2
|
Select%20all/singular: eedc7cdb02de467c15dc418a066a77f2
|
||||||
Select%20source/singular: c7b0ec447415ff62856803a4d7838bd6
|
Select%20source/singular: c7b0ec447415ff62856803a4d7838bd6
|
||||||
Self%20Host/singular: 43fbce2027548b712002e489a20a621a
|
Self%20Host/singular: 43fbce2027548b712002e489a20a621a
|
||||||
Self%20host%20with%20Github/singular: 972155419bcc9f8f9bad9f5ad3538774
|
Self%20host%20with%20Github/singular: 972155419bcc9f8f9bad9f5ad3538774
|
||||||
self-assigned%20the%20card/singular: 24a8d97bcd747643772ebe883be69698
|
self-assigned%20the%20card/singular: 24a8d97bcd747643772ebe883be69698
|
||||||
Send%20feedback/singular: 9631cc08d49da04475b30a0d320ce97c
|
Send/singular: 76a8497d7b546628b03bb81d5c1ce995
|
||||||
Senior/singular: 3fff865dc00435f82896fc302ea45630
|
|
||||||
Settings/singular: 8df6777277469c1fd88cc18dde2f1cc3
|
Settings/singular: 8df6777277469c1fd88cc18dde2f1cc3
|
||||||
Settings%20%7C%20%7B0%7D/singular: b8fc73080bc9c8f4f1403b2a69bd1ac5
|
Settings%20%7C%20%7B0%7D/singular: b8fc73080bc9c8f4f1403b2a69bd1ac5
|
||||||
Sign%20in/singular: cb8757c7450e17de1e226e82fb0fa4a2
|
Sign%20in/singular: cb8757c7450e17de1e226e82fb0fa4a2
|
||||||
@@ -291,11 +237,8 @@ checksums:
|
|||||||
Sign%20up%20with%20/singular: 103e5d75506e6835b471695d799ff580
|
Sign%20up%20with%20/singular: 103e5d75506e6835b471695d799ff580
|
||||||
Simple%20pricing/singular: 5aa2bece93a71947b268498a58062937
|
Simple%20pricing/singular: 5aa2bece93a71947b268498a58062937
|
||||||
Simple%2C%20visual%20task%20management%20that%20just%20works.%20Drag%20and%20drop%20cards%2C%20collaborate%20with%20your%20team%2C%20and%20get%20more%20done./singular: f4f138f83017d43dab2882cc08e07bdb
|
Simple%2C%20visual%20task%20management%20that%20just%20works.%20Drag%20and%20drop%20cards%2C%20collaborate%20with%20your%20team%2C%20and%20get%20more%20done./singular: f4f138f83017d43dab2882cc08e07bdb
|
||||||
Social%20Media/singular: 2227508afb0d38a027e323207e47d4a2
|
|
||||||
Software%20Development/singular: 67659097ef6bcb194839858b2c4f3a3c
|
|
||||||
Star%20on%20Github/singular: e5cb0428f1ac6485688f120d78742875
|
Star%20on%20Github/singular: e5cb0428f1ac6485688f120d78742875
|
||||||
Success/singular: c43827becada6750f7a25890905f38b9
|
Success/singular: c43827becada6750f7a25890905f38b9
|
||||||
Support/singular: 55aab5fd0f31a9cb055a2edeeedfaf63
|
|
||||||
Support%20the%20development%20of%20the%20project/singular: c63bab228ab2b1be4370c668a99cd278
|
Support%20the%20development%20of%20the%20project/singular: c63bab228ab2b1be4370c668a99cd278
|
||||||
System/singular: 803281c327a02a2feba182f4a2b38311
|
System/singular: 803281c327a02a2feba182f4a2b38311
|
||||||
Teams/singular: b63448c05270497973ac4407047dae02
|
Teams/singular: b63448c05270497973ac4407047dae02
|
||||||
@@ -312,12 +255,10 @@ checksums:
|
|||||||
This%20will%20result%20in%20the%20permanent%20deletion%20of%20all%20data%20associated%20with%20this%20workspace./singular: a31141558af793635c1ddd2fa0a33499
|
This%20will%20result%20in%20the%20permanent%20deletion%20of%20all%20data%20associated%20with%20this%20workspace./singular: a31141558af793635c1ddd2fa0a33499
|
||||||
This%20will%20result%20in%20the%20permanent%20deletion%20of%20all%20data%20associated%20with%20your%20account./singular: b49224632bd6c3b7f5e462912aeb1081
|
This%20will%20result%20in%20the%20permanent%20deletion%20of%20all%20data%20associated%20with%20your%20account./singular: b49224632bd6c3b7f5e462912aeb1081
|
||||||
This%20workspace%20username%20has%20already%20been%20taken/singular: b7eadb89c615874f416d9658d0428c4c
|
This%20workspace%20username%20has%20already%20been%20taken/singular: b7eadb89c615874f416d9658d0428c4c
|
||||||
To%20Do/singular: d60813ea824f373462471e092d136eed
|
|
||||||
Toggle%20menu/singular: 29dea3e0b6238874f8c7a27619df8e36
|
Toggle%20menu/singular: 29dea3e0b6238874f8c7a27619df8e36
|
||||||
Track%20all%20card%20changes%20with%20detailed%20activity%20history./singular: 0d3bac559c71ec4b8734f9f212320de5
|
Track%20all%20card%20changes%20with%20detailed%20activity%20history./singular: 0d3bac559c71ec4b8734f9f212320de5
|
||||||
Trello%20disconnected/singular: 54b24a3e6c9a7eedd8c8d1060ab1175d
|
Trello%20disconnected/singular: 54b24a3e6c9a7eedd8c8d1060ab1175d
|
||||||
Trello%20imports/singular: 6827eca403faa8f89891d17827e72af9
|
Trello%20imports/singular: 6827eca403faa8f89891d17827e72af9
|
||||||
Triaging/singular: 1d40799fcae53a8a27688fdae2a48dee
|
|
||||||
Unable%20to%20add%20comment/singular: 49bb435880817434698f31a6069564d6
|
Unable%20to%20add%20comment/singular: 49bb435880817434698f31a6069564d6
|
||||||
Unable%20to%20create%20card/singular: 90112ea12ec6fa42097a6e022ae048a4
|
Unable%20to%20create%20card/singular: 90112ea12ec6fa42097a6e022ae048a4
|
||||||
Unable%20to%20create%20list/singular: 7fbbf8314f8d08a4123c7daef09fed05
|
Unable%20to%20create%20list/singular: 7fbbf8314f8d08a4123c7daef09fed05
|
||||||
@@ -347,13 +288,10 @@ checksums:
|
|||||||
updated%20the%20title/singular: 98cf8f12923ec1a58e767e3a40863b21
|
updated%20the%20title/singular: 98cf8f12923ec1a58e767e3a40863b21
|
||||||
updated%20the%20title%20to%20%3C0%3E%7BtoTitle%7D%3C%2F0%3E/singular: 70cdf681ec2b6ef8e14397bc1f93f945
|
updated%20the%20title%20to%20%3C0%3E%7BtoTitle%7D%3C%2F0%3E/singular: 70cdf681ec2b6ef8e14397bc1f93f945
|
||||||
Upgrade/singular: 63c3b52882e0d779859307d672c178c2
|
Upgrade/singular: 63c3b52882e0d779859307d672c178c2
|
||||||
Urgent/singular: e79afcd4398e52e10e7e8243a7eecabe
|
|
||||||
URL%20can%20only%20contain%20letters%2C%20numbers%2C%20and%20hyphens/singular: 875d37d2d920f98ef60a314eda8a5d9a
|
URL%20can%20only%20contain%20letters%2C%20numbers%2C%20and%20hyphens/singular: 875d37d2d920f98ef60a314eda8a5d9a
|
||||||
URL%20cannot%20exceed%2024%20characters/singular: 395c25732b84ac1f5b63ee310da08e93
|
URL%20cannot%20exceed%2024%20characters/singular: 395c25732b84ac1f5b63ee310da08e93
|
||||||
URL%20must%20be%20at%20least%203%20characters%20long/singular: 1b74ac9ec61fd34b16530ed599009ed1
|
URL%20must%20be%20at%20least%203%20characters%20long/singular: 1b74ac9ec61fd34b16530ed599009ed1
|
||||||
Use%20template/singular: 33f0ca0aa8f4b0de99aad267a61a094d
|
|
||||||
User/singular: 61073457a5c3901084b557d065f876be
|
User/singular: 61073457a5c3901084b557d065f876be
|
||||||
Video/singular: 8050c90e4289b105a0780f0fdda6ff66
|
|
||||||
View%20and%20manage%20your%20API%20keys./singular: 88e23ee79056846dd9ce63ceee690549
|
View%20and%20manage%20your%20API%20keys./singular: 88e23ee79056846dd9ce63ceee690549
|
||||||
View%20and%20manage%20your%20billing%20and%20subscription./singular: 7469502543aac6f02149813546d1cb38
|
View%20and%20manage%20your%20billing%20and%20subscription./singular: 7469502543aac6f02149813546d1cb38
|
||||||
View%20docs/singular: 43ce54f27bb1e3f1880b56e03ab38871
|
View%20docs/singular: 43ce54f27bb1e3f1880b56e03ab38871
|
||||||
@@ -379,7 +317,6 @@ checksums:
|
|||||||
Workspace%20name%20updated/singular: 3206ea410ee1ea4182b27ac0d89f92a1
|
Workspace%20name%20updated/singular: 3206ea410ee1ea4182b27ac0d89f92a1
|
||||||
Workspace%20slug%20updated/singular: 527b92711d38cb35b40741df43aef047
|
Workspace%20slug%20updated/singular: 527b92711d38cb35b40741df43aef047
|
||||||
Workspace%20URL/singular: f4397a838da0f3a44cbd3ebe408ed6c3
|
Workspace%20URL/singular: f4397a838da0f3a44cbd3ebe408ed6c3
|
||||||
Writing/singular: 4f5f3ee3320c252921f80c42653c5d49
|
|
||||||
Yearly/singular: 87f43e016c19cb25860f456549a2f431
|
Yearly/singular: 87f43e016c19cb25860f456549a2f431
|
||||||
Yes%2C%20we%20offer%20an%20forever%20free%20plan%20for%20individual%20use.%20No%20restrictions%2C%20no%20paywalls%2C%20no%20limits./singular: 963cee303d0ff13c1719f4575e774fe3
|
Yes%2C%20we%20offer%20an%20forever%20free%20plan%20for%20individual%20use.%20No%20restrictions%2C%20no%20paywalls%2C%20no%20limits./singular: 963cee303d0ff13c1719f4575e774fe3
|
||||||
You%20can%20get%20a%20custom%20workspace%20URL%2C%20like%20%3C0%3Ekan.bn%2Fkan%3C%2F0%3E%2C%20by%20going%20into%20your%20%3C1%3Eworkspace%20settings%3C%2F1%3E%20and%20purchasing%20a%20pro%20workspace%20subscription.%20All%20subscriptions%20help%20fund%20the%20development%20of%20the%20project!/singular: 41dd145ef56f539e12dc064a9807c660
|
You%20can%20get%20a%20custom%20workspace%20URL%2C%20like%20%3C0%3Ekan.bn%2Fkan%3C%2F0%3E%2C%20by%20going%20into%20your%20%3C1%3Eworkspace%20settings%3C%2F1%3E%20and%20purchasing%20a%20pro%20workspace%20subscription.%20All%20subscriptions%20help%20fund%20the%20development%20of%20the%20project!/singular: 41dd145ef56f539e12dc064a9807c660
|
||||||
|
|||||||
@@ -31,12 +31,9 @@ const config = {
|
|||||||
|
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
|
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: process.env.S3_FORCE_PATH_STYLE === "true"
|
hostname: `*.${env("NEXT_PUBLIC_STORAGE_DOMAIN")}`,
|
||||||
? `${env("NEXT_PUBLIC_STORAGE_DOMAIN")}`
|
|
||||||
: `*.${env("NEXT_PUBLIC_STORAGE_DOMAIN")}`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
protocol: "http",
|
protocol: "http",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 400 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 433 KiB |
@@ -259,7 +259,6 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={key}
|
|
||||||
onClick={() => handleLoginWithProvider(key as SocialProvider)}
|
onClick={() => handleLoginWithProvider(key as SocialProvider)}
|
||||||
isLoading={isLoginWithProviderPending === key}
|
isLoading={isLoginWithProviderPending === key}
|
||||||
iconLeft={<provider.icon />}
|
iconLeft={<provider.icon />}
|
||||||
|
|||||||
@@ -1,96 +1,11 @@
|
|||||||
import { useRef, useState } from "react";
|
import Link from "next/link";
|
||||||
import {
|
|
||||||
TbLayoutSidebarLeftCollapse,
|
|
||||||
TbLayoutSidebarLeftExpand,
|
|
||||||
TbLayoutSidebarRightCollapse,
|
|
||||||
TbLayoutSidebarRightExpand,
|
|
||||||
} from "react-icons/tb";
|
|
||||||
|
|
||||||
import { authClient } from "@kan/auth/client";
|
import { api } from "~/utils/api";
|
||||||
|
import FeedbackButton from "./FeedbackButton";
|
||||||
import { useClickOutside } from "~/hooks/useClickOutside";
|
|
||||||
import { useModal } from "~/providers/modal";
|
|
||||||
import { useTheme } from "~/providers/theme";
|
|
||||||
import { WorkspaceProvider } from "~/providers/workspace";
|
|
||||||
import FeedbackModal from "./FeedbackModal";
|
|
||||||
import Modal from "./modal";
|
|
||||||
import SideNavigation from "./SideNavigation";
|
import SideNavigation from "./SideNavigation";
|
||||||
|
|
||||||
interface DashboardProps {
|
export default function Dashboard(props: { children: React.ReactNode }) {
|
||||||
children: React.ReactNode;
|
const { data, isLoading } = api.user.getUser.useQuery();
|
||||||
rightPanel?: React.ReactNode;
|
|
||||||
hasRightPanel?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDashboardLayout(
|
|
||||||
page: React.ReactElement,
|
|
||||||
rightPanel?: React.ReactNode,
|
|
||||||
hasRightPanel = false,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<WorkspaceProvider>
|
|
||||||
<Dashboard rightPanel={rightPanel} hasRightPanel={hasRightPanel}>
|
|
||||||
{page}
|
|
||||||
</Dashboard>
|
|
||||||
</WorkspaceProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Dashboard({
|
|
||||||
children,
|
|
||||||
rightPanel,
|
|
||||||
hasRightPanel = false,
|
|
||||||
}: DashboardProps) {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { modalContentType } = useModal();
|
|
||||||
|
|
||||||
const { data: session, isPending: sessionLoading } = authClient.useSession();
|
|
||||||
|
|
||||||
const [isSideNavOpen, setIsSideNavOpen] = useState(false);
|
|
||||||
const [isRightPanelOpen, setIsRightPanelOpen] = useState(false);
|
|
||||||
|
|
||||||
const sideNavRef = useRef<HTMLDivElement>(null);
|
|
||||||
const rightPanelRef = useRef<HTMLDivElement>(null);
|
|
||||||
const sideNavButtonRef = useRef<HTMLButtonElement>(null);
|
|
||||||
const rightPanelButtonRef = useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
const toggleSideNav = () => {
|
|
||||||
setIsSideNavOpen(!isSideNavOpen);
|
|
||||||
if (!isSideNavOpen) {
|
|
||||||
setIsRightPanelOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeSideNav = () => {
|
|
||||||
setIsSideNavOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleRightPanel = () => {
|
|
||||||
setIsRightPanelOpen(!isRightPanelOpen);
|
|
||||||
if (!isRightPanelOpen) {
|
|
||||||
setIsSideNavOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useClickOutside(sideNavRef, (event) => {
|
|
||||||
if (sideNavButtonRef.current?.contains(event.target as Node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isSideNavOpen) {
|
|
||||||
setIsSideNavOpen(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useClickOutside(rightPanelRef, (event) => {
|
|
||||||
if (rightPanelButtonRef.current?.contains(event.target as Node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isRightPanelOpen) {
|
|
||||||
setIsRightPanelOpen(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const isDarkMode = theme.activeTheme === "dark";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -98,86 +13,26 @@ export default function Dashboard({
|
|||||||
html {
|
html {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 320px;
|
|
||||||
background-color: ${!isDarkMode ? "hsl(0deg 0% 97.3%)" : "#1c1c1c"};
|
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<div className="relative flex h-screen flex-col bg-light-50 dark:bg-dark-50 md:bg-light-100 md:p-3 md:dark:bg-dark-100">
|
<div className="flex h-screen min-w-[800px] flex-col items-center bg-light-100 dark:bg-dark-50">
|
||||||
{/* Mobile Header */}
|
<div className="m-auto flex h-16 min-h-16 w-full justify-between border-b border-light-600 px-5 py-2 align-middle dark:border-dark-400">
|
||||||
<div className="flex h-12 items-center justify-between border-b border-light-300 bg-light-50 px-3 dark:border-dark-300 dark:bg-dark-50 md:hidden">
|
<div className="my-auto flex w-full items-center justify-between">
|
||||||
<button
|
<Link href="/">
|
||||||
ref={sideNavButtonRef}
|
<h1 className="text-lg font-bold tracking-tight text-neutral-900 dark:text-dark-1000">
|
||||||
onClick={toggleSideNav}
|
kan.bn
|
||||||
className="rounded p-1.5 transition-all hover:bg-light-200 dark:hover:bg-dark-100"
|
</h1>
|
||||||
>
|
</Link>
|
||||||
{isSideNavOpen ? (
|
<FeedbackButton />
|
||||||
<TbLayoutSidebarLeftCollapse
|
</div>
|
||||||
size={20}
|
|
||||||
className="text-light-900 dark:text-dark-900"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TbLayoutSidebarLeftExpand
|
|
||||||
size={20}
|
|
||||||
className="text-light-900 dark:text-dark-900"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{hasRightPanel && (
|
|
||||||
<button
|
|
||||||
ref={rightPanelButtonRef}
|
|
||||||
onClick={toggleRightPanel}
|
|
||||||
className="rounded p-1.5 transition-all hover:bg-light-200 dark:hover:bg-dark-100"
|
|
||||||
>
|
|
||||||
{isRightPanelOpen ? (
|
|
||||||
<TbLayoutSidebarRightCollapse
|
|
||||||
size={20}
|
|
||||||
className="text-light-900 dark:text-dark-900"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TbLayoutSidebarRightExpand
|
|
||||||
size={20}
|
|
||||||
className="text-light-900 dark:text-dark-900"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex h-[calc(100dvh-4.5rem)] min-h-0 w-full md:h-[calc(100dvh-1.5rem)]">
|
<div className="flex h-full w-full">
|
||||||
<div
|
<SideNavigation
|
||||||
ref={sideNavRef}
|
user={{ email: data?.email, image: data?.image }}
|
||||||
className={`fixed top-12 z-40 h-[calc(100dvh-3rem)] w-[calc(100vw-1.5rem)] transform transition-transform duration-300 ease-in-out md:relative md:top-0 md:h-full md:w-auto md:translate-x-0 ${isSideNavOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"} `}
|
isLoading={isLoading}
|
||||||
>
|
/>
|
||||||
<SideNavigation
|
<div className="w-full overflow-hidden">{props.children}</div>
|
||||||
user={{ email: session?.user.email, image: session?.user.image }}
|
|
||||||
isLoading={sessionLoading}
|
|
||||||
onCloseSideNav={closeSideNav}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative h-full min-h-0 w-full overflow-hidden md:rounded-lg md:border md:border-light-300 md:bg-light-50 md:dark:border-dark-300 md:dark:bg-dark-50">
|
|
||||||
<div className="relative flex h-full min-h-0 w-full overflow-hidden">
|
|
||||||
<div className="h-full w-full overflow-y-auto">{children}</div>
|
|
||||||
|
|
||||||
{/* Mobile Right Panel */}
|
|
||||||
{hasRightPanel && rightPanel && (
|
|
||||||
<div
|
|
||||||
ref={rightPanelRef}
|
|
||||||
className={`fixed right-0 top-12 z-40 h-[calc(100dvh-3rem)] w-80 transform border-l border-light-300 bg-light-200 transition-transform duration-300 ease-in-out dark:border-dark-300 dark:bg-dark-100 md:hidden ${
|
|
||||||
isRightPanelOpen ? "translate-x-0" : "translate-x-full"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="h-full">{rightPanel}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Desktop Right Panel */}
|
|
||||||
{hasRightPanel && rightPanel && (
|
|
||||||
<div className="hidden md:block">{rightPanel}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -350,6 +350,7 @@ export default function Editor({
|
|||||||
<div ref={containerRef}>
|
<div ref={containerRef}>
|
||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
.tiptap p.is-empty::before {
|
.tiptap p.is-empty::before {
|
||||||
|
color: #adb5bd;
|
||||||
content: attr(data-placeholder);
|
content: attr(data-placeholder);
|
||||||
float: left;
|
float: left;
|
||||||
height: 0;
|
height: 0;
|
||||||
@@ -362,7 +363,7 @@ export default function Editor({
|
|||||||
{!readOnly && editor && <EditorBubbleMenu editor={editor} />}
|
{!readOnly && editor && <EditorBubbleMenu editor={editor} />}
|
||||||
<EditorContent
|
<EditorContent
|
||||||
editor={editor}
|
editor={editor}
|
||||||
className="prose dark:prose-invert prose-sm max-w-none overflow-y-auto [&_blockquote]:!text-xs [&_h1]:!text-lg [&_h2]:!text-base [&_h3]:!text-sm [&_ol]:!text-xs [&_p.is-empty::before]:text-light-900 [&_p.is-empty::before]:dark:text-dark-800 [&_p]:!text-sm [&_p]:text-black [&_p]:dark:text-white [&_ul]:!text-xs"
|
className="prose dark:prose-invert prose-sm max-w-none overflow-y-auto [&_blockquote]:!text-xs [&_h1]:!text-lg [&_h2]:!text-base [&_h3]:!text-sm [&_ol]:!text-xs [&_p.is-empty::before]:text-dark-800 [&_p.is-empty::before]:dark:text-dark-800 [&_p]:!text-sm [&_p]:text-black [&_p]:dark:text-white [&_ul]:!text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
149
apps/web/src/components/FeedbackButton.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
|
import { Fragment, useRef, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import chatIconDark from "~/assets/chat-dark.json";
|
||||||
|
import chatIconLight from "~/assets/chat-light.json";
|
||||||
|
import Button from "~/components/Button";
|
||||||
|
import Input from "~/components/Input";
|
||||||
|
import LottieIcon from "~/components/LottieIcon";
|
||||||
|
import { usePopup } from "~/providers/popup";
|
||||||
|
import { useTheme } from "~/providers/theme";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
|
interface NewFeedbackFormInput {
|
||||||
|
feedback: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeedbackButton: React.FC = () => {
|
||||||
|
const outsideRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { activeTheme } = useTheme();
|
||||||
|
const { showPopup } = usePopup();
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
|
||||||
|
const { handleSubmit, setValue, watch, reset } =
|
||||||
|
useForm<NewFeedbackFormInput>({
|
||||||
|
defaultValues: {
|
||||||
|
feedback: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createFeedback = api.feedback.create.useMutation({
|
||||||
|
onSuccess: async () => {
|
||||||
|
reset();
|
||||||
|
showPopup({
|
||||||
|
header: t`Feedback sent`,
|
||||||
|
message: t`Thank you for your feedback!`,
|
||||||
|
icon: "success",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: async () => {
|
||||||
|
showPopup({
|
||||||
|
header: t`Unable to send feedback`,
|
||||||
|
message: t`Please try again later, or contact customer support.`,
|
||||||
|
icon: "error",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
setIsHovered(true);
|
||||||
|
setIndex((index) => index + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = (values: NewFeedbackFormInput) => {
|
||||||
|
createFeedback.mutate({
|
||||||
|
feedback: values.feedback,
|
||||||
|
url: window.location.href,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={outsideRef} />
|
||||||
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
|
<div>
|
||||||
|
<Menu.Button
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
className="flex items-center rounded-md border-[1px] border-light-600 bg-light-50 px-2.5 py-1.5 text-sm font-normal text-neutral-900 shadow-sm dark:border-dark-400 dark:bg-dark-50 dark:text-dark-1000"
|
||||||
|
>
|
||||||
|
<LottieIcon
|
||||||
|
index={index}
|
||||||
|
json={activeTheme === "dark" ? chatIconDark : chatIconLight}
|
||||||
|
isPlaying={isHovered}
|
||||||
|
/>
|
||||||
|
<span className="ml-1">{t`Feedback`}</span>
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items
|
||||||
|
as="div"
|
||||||
|
static
|
||||||
|
className="absolute right-0 z-30 mt-2 w-[350px] origin-top-right rounded-md border border-light-200 bg-light-50 p-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-400 dark:bg-dark-300"
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="p-1">
|
||||||
|
<Input
|
||||||
|
placeholder={t`Ideas to improve this page...`}
|
||||||
|
onChange={(e) => {
|
||||||
|
setValue("feedback", e.target.value);
|
||||||
|
}}
|
||||||
|
value={watch("feedback")}
|
||||||
|
contentEditable
|
||||||
|
className="max-h-[300px] min-h-[100px]"
|
||||||
|
onKeyDown={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.key === "Enter" && e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
await handleSubmit(onSubmit)();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-row items-center justify-between pt-2">
|
||||||
|
<div>
|
||||||
|
<p className="ml-2 text-xs text-neutral-900 dark:text-dark-1000">
|
||||||
|
{t`Need help?`}{" "}
|
||||||
|
<Link
|
||||||
|
href="mailto:support@kan.bn"
|
||||||
|
className="text-blue-600 underline dark:text-blue-300"
|
||||||
|
>
|
||||||
|
{t`Contact us`}
|
||||||
|
</Link>
|
||||||
|
{t`, or see our`}{" "}
|
||||||
|
<Link
|
||||||
|
href="https://docs.kan.bn"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-blue-600 underline dark:text-blue-300"
|
||||||
|
>
|
||||||
|
{t`docs`}
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="justify-end">
|
||||||
|
<Button size="sm" isLoading={createFeedback.isPending}>
|
||||||
|
{t`Send`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeedbackButton;
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { t } from "@lingui/core/macro";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { HiXMark } from "react-icons/hi2";
|
|
||||||
|
|
||||||
import Button from "~/components/Button";
|
|
||||||
import Input from "~/components/Input";
|
|
||||||
import { useModal } from "~/providers/modal";
|
|
||||||
import { usePopup } from "~/providers/popup";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
|
|
||||||
interface NewFeedbackFormInput {
|
|
||||||
feedback: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FeedbackModal() {
|
|
||||||
const { closeModal } = useModal();
|
|
||||||
const { showPopup } = usePopup();
|
|
||||||
|
|
||||||
const { handleSubmit, setValue, watch, reset } =
|
|
||||||
useForm<NewFeedbackFormInput>({
|
|
||||||
defaultValues: {
|
|
||||||
feedback: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const createFeedback = api.feedback.create.useMutation({
|
|
||||||
onSuccess: async () => {
|
|
||||||
reset();
|
|
||||||
closeModal();
|
|
||||||
showPopup({
|
|
||||||
header: t`Feedback sent`,
|
|
||||||
message: t`Thank you for your feedback!`,
|
|
||||||
icon: "success",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: async () => {
|
|
||||||
showPopup({
|
|
||||||
header: t`Unable to send feedback`,
|
|
||||||
message: t`Please try again later, or contact customer support.`,
|
|
||||||
icon: "error",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = (values: NewFeedbackFormInput) => {
|
|
||||||
createFeedback.mutate({
|
|
||||||
feedback: values.feedback,
|
|
||||||
url: window.location.href,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<div className="px-5 pt-5">
|
|
||||||
<div className="flex w-full items-center justify-between pb-4">
|
|
||||||
<h2 className="text-sm font-bold text-neutral-900 dark:text-dark-1000">
|
|
||||||
{t`Feedback`}
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="rounded p-1 hover:bg-light-200 focus:outline-none dark:hover:bg-dark-300"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
closeModal();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
id="feedback"
|
|
||||||
placeholder={t`Ideas to improve this page...`}
|
|
||||||
onChange={(e) => setValue("feedback", e.target.value)}
|
|
||||||
contentEditable
|
|
||||||
value={watch("feedback")}
|
|
||||||
className="min-h-[120px]"
|
|
||||||
onKeyDown={async (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (e.key === "Enter" && e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
await handleSubmit(onSubmit)();
|
|
||||||
}
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6 flex items-center justify-between border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
|
||||||
<div className="text-xs text-neutral-600 dark:text-dark-800">
|
|
||||||
<p>
|
|
||||||
{t`Need help?`}{" "}
|
|
||||||
<Link
|
|
||||||
href="mailto:support@kan.bn"
|
|
||||||
className="text-blue-600 underline dark:text-blue-300"
|
|
||||||
>
|
|
||||||
{t`Contact us`}
|
|
||||||
</Link>
|
|
||||||
{t`, or see our`}{" "}
|
|
||||||
<Link
|
|
||||||
href="https://docs.kan.bn"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-blue-600 underline dark:text-blue-300"
|
|
||||||
>
|
|
||||||
{t`docs`}
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Button type="submit" isLoading={createFeedback.isPending}>
|
|
||||||
{t`Send feedback`}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -55,7 +55,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
<div className="flex w-full flex-col gap-1">
|
<div className="flex w-full flex-col gap-1">
|
||||||
<div className="relative flex">
|
<div className="relative flex">
|
||||||
{prefix && (
|
{prefix && (
|
||||||
<div className="flex shrink-0 items-center rounded-l-md border border-r-0 border-light-600 px-3 text-sm dark:border-dark-700 dark:text-dark-1000 sm:text-sm/6">
|
<div className="flex shrink-0 items-center rounded-l-md border border-r-0 border-light-600 px-3 text-base dark:border-dark-700 dark:text-dark-1000 sm:text-sm/6">
|
||||||
{prefix}
|
{prefix}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import dynamic from "next/dynamic";
|
|||||||
|
|
||||||
const Lottie = dynamic(() => import("react-lottie-player"), { ssr: false });
|
const Lottie = dynamic(() => import("react-lottie-player"), { ssr: false });
|
||||||
|
|
||||||
interface IconProps {
|
type IconProps = {
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
index: number;
|
index: number;
|
||||||
json: object;
|
json: object;
|
||||||
}
|
};
|
||||||
|
|
||||||
const Icon: React.FC<IconProps> = ({ isPlaying, index, json }) => {
|
const Icon: React.FC<IconProps> = ({ isPlaying, index, json }) => {
|
||||||
return (
|
return (
|
||||||
@@ -15,7 +15,7 @@ const Icon: React.FC<IconProps> = ({ isPlaying, index, json }) => {
|
|||||||
animationData={json}
|
animationData={json}
|
||||||
play={isPlaying}
|
play={isPlaying}
|
||||||
loop={false}
|
loop={false}
|
||||||
style={{ width: 18, height: 18, fill: "white" }}
|
style={{ width: 20, height: 20, fill: "white" }}
|
||||||
rendererSettings={{ preserveAspectRatio: "xMidYMid slice" }}
|
rendererSettings={{ preserveAspectRatio: "xMidYMid slice" }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,12 +19,10 @@ export function NewWorkspaceForm() {
|
|||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
const { switchWorkspace } = useWorkspace();
|
const { switchWorkspace } = useWorkspace();
|
||||||
const { register, handleSubmit } = useForm<FormValues>();
|
const { register, handleSubmit } = useForm<FormValues>();
|
||||||
const utils = api.useUtils();
|
|
||||||
|
|
||||||
const createWorkspace = api.workspace.create.useMutation({
|
const createWorkspace = api.workspace.create.useMutation({
|
||||||
onSuccess: (values) => {
|
onSuccess: (values) => {
|
||||||
if (values.publicId && values.name) {
|
if (values.publicId && values.name) {
|
||||||
utils.workspace.all.invalidate();
|
|
||||||
switchWorkspace({
|
switchWorkspace({
|
||||||
publicId: values.publicId,
|
publicId: values.publicId,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
|
|||||||
@@ -4,10 +4,6 @@ export const PageHead = ({ title }: { title: string }) => {
|
|||||||
return (
|
return (
|
||||||
<Head>
|
<Head>
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1, maximum-scale=1"
|
|
||||||
/>
|
|
||||||
</Head>
|
</Head>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ const PatternedBackground = () => (
|
|||||||
<svg className="h-full w-full">
|
<svg className="h-full w-full">
|
||||||
<pattern
|
<pattern
|
||||||
id="pattern"
|
id="pattern"
|
||||||
x="10"
|
x="0.034759358288862785"
|
||||||
y="10"
|
y="3.335370511841166"
|
||||||
width="14.423223834988539"
|
width="14.423223834988539"
|
||||||
height="14.423223834988539"
|
height="14.423223834988539"
|
||||||
patternUnits="userSpaceOnUse"
|
patternUnits="userSpaceOnUse"
|
||||||
|
|||||||
@@ -1,51 +1,37 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import LottieIcon from "~/components/LottieIcon";
|
import LottieIcon from "~/components/LottieIcon";
|
||||||
import { useIsMobile } from "~/hooks/useMediaQuery";
|
|
||||||
|
function classNames(...classes: string[]): string {
|
||||||
|
return classes.filter(Boolean).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
const Button: React.FC<{
|
const Button: React.FC<{
|
||||||
href: string;
|
href: string;
|
||||||
current: boolean;
|
current: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
json: object;
|
json: object;
|
||||||
isCollapsed?: boolean;
|
}> = ({ href, current, name, json }) => {
|
||||||
onCloseSideNav?: () => void;
|
|
||||||
}> = ({ href, current, name, json, isCollapsed = false, onCloseSideNav }) => {
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const [index, setIndex] = useState(0);
|
const [index, setIndex] = useState(0);
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
setIsHovered(true);
|
setIsHovered(true);
|
||||||
setIndex((index) => index + 1);
|
setIndex((index) => index + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
if (onCloseSideNav && isMobile) {
|
|
||||||
onCloseSideNav();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onClick={handleClick}
|
className={classNames(
|
||||||
className={twMerge(
|
current ? "bg-light-200 dark:bg-dark-200" : "dark:bg-dark-50",
|
||||||
"group flex h-[34px] items-center rounded-md p-1.5 text-sm font-normal leading-6 hover:bg-light-200 hover:text-light-1000 dark:hover:bg-dark-200 dark:hover:text-dark-1000",
|
"group flex items-center gap-x-3 rounded-md p-1.5 text-sm font-normal leading-6 text-neutral-900 hover:bg-light-200 dark:text-dark-1000 dark:hover:bg-dark-200",
|
||||||
current
|
|
||||||
? "bg-light-200 text-light-1000 dark:bg-dark-200 dark:text-dark-1000"
|
|
||||||
: "text-neutral-600 dark:bg-dark-100 dark:text-dark-900",
|
|
||||||
isCollapsed
|
|
||||||
? "justify-start gap-x-3 md:justify-center md:gap-x-0"
|
|
||||||
: "gap-x-3",
|
|
||||||
)}
|
)}
|
||||||
title={isCollapsed ? name : undefined}
|
|
||||||
>
|
>
|
||||||
<LottieIcon index={index} json={json} isPlaying={isHovered} />
|
<LottieIcon index={index} json={json} isPlaying={isHovered} />
|
||||||
<span className={twMerge(isCollapsed && "md:hidden")}>{name}</span>
|
{name}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Button } from "@headlessui/react";
|
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import {
|
|
||||||
TbLayoutSidebarLeftCollapse,
|
|
||||||
TbLayoutSidebarLeftExpand,
|
|
||||||
} from "react-icons/tb";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import boardsIconDark from "~/assets/boards-dark.json";
|
import boardsIconDark from "~/assets/boards-dark.json";
|
||||||
import boardsIconLight from "~/assets/boards-light.json";
|
import boardsIconLight from "~/assets/boards-light.json";
|
||||||
@@ -23,7 +15,6 @@ import { useTheme } from "~/providers/theme";
|
|||||||
interface SideNavigationProps {
|
interface SideNavigationProps {
|
||||||
user: UserType;
|
user: UserType;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
onCloseSideNav?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserType {
|
interface UserType {
|
||||||
@@ -34,28 +25,8 @@ interface UserType {
|
|||||||
export default function SideNavigation({
|
export default function SideNavigation({
|
||||||
user,
|
user,
|
||||||
isLoading,
|
isLoading,
|
||||||
onCloseSideNav,
|
|
||||||
}: SideNavigationProps) {
|
}: SideNavigationProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
||||||
const [isInitialised, setIsInitialised] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const savedState = localStorage.getItem("kan_sidebar-collapsed");
|
|
||||||
if (savedState !== null) {
|
|
||||||
setIsCollapsed(JSON.parse(savedState));
|
|
||||||
}
|
|
||||||
setIsInitialised(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isInitialised) {
|
|
||||||
localStorage.setItem(
|
|
||||||
"kan_sidebar-collapsed",
|
|
||||||
JSON.stringify(isCollapsed),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [isCollapsed, isInitialised]);
|
|
||||||
|
|
||||||
const { pathname } = router;
|
const { pathname } = router;
|
||||||
|
|
||||||
@@ -81,50 +52,11 @@ export default function SideNavigation({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const toggleCollapse = () => {
|
|
||||||
setIsCollapsed(!isCollapsed);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav
|
<nav className="flex w-72 flex-col justify-between border-r border-light-600 px-3 pb-3 pt-5 dark:border-dark-400">
|
||||||
className={twMerge(
|
|
||||||
"flex h-full w-64 flex-col justify-between border-r border-light-300 bg-light-100 p-3 dark:border-dark-300 dark:bg-dark-100 md:border-r-0 md:py-0 md:pl-0",
|
|
||||||
isCollapsed && "md:w-auto",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<div className="hidden h-14 items-center justify-between pb-[18px] pt-1.5 md:flex">
|
<WorkspaceMenu />
|
||||||
{!isCollapsed && (
|
|
||||||
<Link href="/" className="block">
|
|
||||||
<h1 className="pl-2 text-lg font-bold tracking-tight text-neutral-900 dark:text-dark-1000">
|
|
||||||
kan.bn
|
|
||||||
</h1>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
onClick={toggleCollapse}
|
|
||||||
className={twMerge(
|
|
||||||
"flex h-8 items-center justify-center rounded-md hover:bg-light-200 dark:hover:bg-dark-200",
|
|
||||||
isCollapsed ? "w-full" : "w-8",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isCollapsed ? (
|
|
||||||
<TbLayoutSidebarLeftExpand
|
|
||||||
size={18}
|
|
||||||
className="text-light-900 dark:text-dark-900"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TbLayoutSidebarLeftCollapse
|
|
||||||
size={18}
|
|
||||||
className="text-light-900 dark:text-dark-900"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="mx-1 mb-4 hidden w-auto border-b border-light-300 dark:border-dark-400 md:block" />
|
|
||||||
|
|
||||||
<WorkspaceMenu isCollapsed={isCollapsed} />
|
|
||||||
<ul role="list" className="space-y-1">
|
<ul role="list" className="space-y-1">
|
||||||
{navigation.map((item) => (
|
{navigation.map((item) => (
|
||||||
<li key={item.name}>
|
<li key={item.name}>
|
||||||
@@ -133,23 +65,16 @@ export default function SideNavigation({
|
|||||||
current={pathname.includes(item.href)}
|
current={pathname.includes(item.href)}
|
||||||
name={item.name}
|
name={item.name}
|
||||||
json={item.icon}
|
json={item.icon}
|
||||||
isCollapsed={isCollapsed}
|
|
||||||
onCloseSideNav={onCloseSideNav}
|
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<UserMenu
|
||||||
<div className="space-y-3">
|
email={user.email ?? ""}
|
||||||
<UserMenu
|
imageUrl={user.image ?? undefined}
|
||||||
email={user.email ?? ""}
|
isLoading={isLoading}
|
||||||
imageUrl={user.image ?? undefined}
|
/>
|
||||||
isLoading={isLoading}
|
|
||||||
isCollapsed={isCollapsed}
|
|
||||||
onCloseSideNav={onCloseSideNav}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import { authClient } from "@kan/auth/client";
|
import { authClient } from "@kan/auth/client";
|
||||||
|
|
||||||
import { useIsMobile } from "~/hooks/useMediaQuery";
|
|
||||||
import { useModal } from "~/providers/modal";
|
|
||||||
import { useTheme } from "~/providers/theme";
|
import { useTheme } from "~/providers/theme";
|
||||||
import { getAvatarUrl } from "~/utils/helpers";
|
import { getAvatarUrl } from "~/utils/helpers";
|
||||||
|
|
||||||
@@ -17,63 +13,38 @@ interface UserMenuProps {
|
|||||||
imageUrl: string | undefined;
|
imageUrl: string | undefined;
|
||||||
email: string;
|
email: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isCollapsed?: boolean;
|
}
|
||||||
onCloseSideNav?: () => void;
|
|
||||||
|
function classNames(...classes: string[]): string {
|
||||||
|
return classes.filter(Boolean).join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UserMenu({
|
export default function UserMenu({
|
||||||
imageUrl,
|
imageUrl,
|
||||||
email,
|
email,
|
||||||
isLoading,
|
isLoading,
|
||||||
isCollapsed = false,
|
|
||||||
onCloseSideNav,
|
|
||||||
}: UserMenuProps) {
|
}: UserMenuProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { themePreference, switchTheme } = useTheme();
|
const { themePreference, switchTheme } = useTheme();
|
||||||
const { openModal } = useModal();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
if (onCloseSideNav && isMobile) {
|
|
||||||
onCloseSideNav();
|
|
||||||
}
|
|
||||||
await authClient.signOut();
|
await authClient.signOut();
|
||||||
|
|
||||||
router.push("/login");
|
router.push("/login");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLinkClick = () => {
|
|
||||||
if (onCloseSideNav && isMobile) {
|
|
||||||
onCloseSideNav();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleModalOpen = (modalType: string) => {
|
|
||||||
if (onCloseSideNav && isMobile) {
|
|
||||||
onCloseSideNav();
|
|
||||||
}
|
|
||||||
openModal(modalType);
|
|
||||||
};
|
|
||||||
|
|
||||||
const avatarUrl = imageUrl ? getAvatarUrl(imageUrl) : null;
|
const avatarUrl = imageUrl ? getAvatarUrl(imageUrl) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu as="div" className="relative inline-block w-full text-left">
|
<Menu as="div" className="relative inline-block w-full text-left">
|
||||||
<div>
|
<div>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className={twMerge(!isCollapsed && "flex")}>
|
<div className="flex">
|
||||||
<div className="h-[30px] w-[30px] animate-pulse rounded-full bg-light-200 dark:bg-dark-200" />
|
<div className="h-[30px] w-[30px] animate-pulse rounded-full bg-light-200 dark:bg-dark-200" />
|
||||||
<div
|
<div className="mx-2 h-[30px] w-[175px] animate-pulse rounded-md bg-light-200 dark:bg-dark-200" />
|
||||||
className={twMerge(
|
|
||||||
"mx-2 h-[30px] w-[175px] animate-pulse rounded-md bg-light-200 dark:bg-dark-200",
|
|
||||||
isCollapsed && "md:hidden",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Menu.Button
|
<Menu.Button className="flex w-full items-center rounded-md p-1.5 text-neutral-900 hover:bg-light-200 dark:text-dark-900 dark:hover:bg-dark-200 dark:hover:text-dark-1000">
|
||||||
className="flex w-full items-center rounded-md p-1.5 text-neutral-900 hover:bg-light-200 dark:text-dark-900 dark:hover:bg-dark-200 dark:hover:text-dark-1000"
|
|
||||||
title={isCollapsed ? email : undefined}
|
|
||||||
>
|
|
||||||
{avatarUrl ? (
|
{avatarUrl ? (
|
||||||
<Image
|
<Image
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
@@ -93,14 +64,7 @@ export default function UserMenu({
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span
|
<span className="mx-2 truncate text-sm">{email}</span>
|
||||||
className={twMerge(
|
|
||||||
"mx-2 truncate text-sm",
|
|
||||||
isCollapsed && "md:hidden",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{email}
|
|
||||||
</span>
|
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -114,12 +78,7 @@ export default function UserMenu({
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items
|
<Menu.Items className="absolute bottom-[40px] left-0 z-10 mt-2 w-full origin-top-left rounded-md border border-light-600 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-600 dark:bg-dark-300">
|
||||||
className={twMerge(
|
|
||||||
"absolute bottom-[40px] z-10 mt-2 origin-top-left rounded-md border border-light-600 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-600 dark:bg-dark-300",
|
|
||||||
isCollapsed ? "left-0 w-48" : "left-0 w-full",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col text-neutral-900 dark:text-dark-1000">
|
<div className="flex flex-col text-neutral-900 dark:text-dark-1000">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
<div className="flex w-full items-center px-3 py-2 text-left text-xs">
|
<div className="flex w-full items-center px-3 py-2 text-left text-xs">
|
||||||
@@ -131,7 +90,7 @@ export default function UserMenu({
|
|||||||
className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400"
|
className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={twMerge(
|
className={classNames(
|
||||||
themePreference === "system" ? "visible" : "invisible",
|
themePreference === "system" ? "visible" : "invisible",
|
||||||
"mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900",
|
"mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900",
|
||||||
)}
|
)}
|
||||||
@@ -145,7 +104,7 @@ export default function UserMenu({
|
|||||||
className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400"
|
className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={twMerge(
|
className={classNames(
|
||||||
themePreference === "dark" ? "visible" : "invisible",
|
themePreference === "dark" ? "visible" : "invisible",
|
||||||
"mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900",
|
"mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900",
|
||||||
)}
|
)}
|
||||||
@@ -159,7 +118,7 @@ export default function UserMenu({
|
|||||||
className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400"
|
className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={twMerge(
|
className={classNames(
|
||||||
themePreference === "light" ? "visible" : "invisible",
|
themePreference === "light" ? "visible" : "invisible",
|
||||||
"mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900",
|
"mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900",
|
||||||
)}
|
)}
|
||||||
@@ -168,38 +127,6 @@ export default function UserMenu({
|
|||||||
</button>
|
</button>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</div>
|
</div>
|
||||||
<div className="light-border-600 border-t-[1px] p-1 dark:border-dark-600">
|
|
||||||
<Menu.Item>
|
|
||||||
<Link
|
|
||||||
href="mailto:support@kan.bn"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
onClick={handleLinkClick}
|
|
||||||
className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400"
|
|
||||||
>
|
|
||||||
{t`Support`}
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item>
|
|
||||||
<Link
|
|
||||||
href="https://docs.kan.bn"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
onClick={handleLinkClick}
|
|
||||||
className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400"
|
|
||||||
>
|
|
||||||
{t`Documentation`}
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item>
|
|
||||||
<button
|
|
||||||
onClick={() => handleModalOpen("NEW_FEEDBACK")}
|
|
||||||
className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400"
|
|
||||||
>
|
|
||||||
{t`Feedback`}
|
|
||||||
</button>
|
|
||||||
</Menu.Item>
|
|
||||||
</div>
|
|
||||||
<div className="light-border-600 border-t-[1px] p-1 dark:border-dark-600">
|
<div className="light-border-600 border-t-[1px] p-1 dark:border-dark-600">
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -2,16 +2,11 @@ import { Menu, Transition } from "@headlessui/react";
|
|||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { HiCheck } from "react-icons/hi2";
|
import { HiCheck } from "react-icons/hi2";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { useWorkspace } from "~/providers/workspace";
|
import { useWorkspace } from "~/providers/workspace";
|
||||||
|
|
||||||
export default function WorkspaceMenu({
|
export default function WorkspaceMenu() {
|
||||||
isCollapsed = false,
|
|
||||||
}: {
|
|
||||||
isCollapsed?: boolean;
|
|
||||||
}) {
|
|
||||||
const { workspace, isLoading, availableWorkspaces, switchWorkspace } =
|
const { workspace, isLoading, availableWorkspaces, switchWorkspace } =
|
||||||
useWorkspace();
|
useWorkspace();
|
||||||
const { openModal } = useModal();
|
const { openModal } = useModal();
|
||||||
@@ -20,43 +15,22 @@ export default function WorkspaceMenu({
|
|||||||
<Menu as="div" className="relative inline-block w-full pb-3 text-left">
|
<Menu as="div" className="relative inline-block w-full pb-3 text-left">
|
||||||
<div>
|
<div>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className={twMerge("mb-1", isCollapsed && "md:flex md:p-1.5")}>
|
<div className="mb-1 flex p-1.5">
|
||||||
<div className="h-6 w-6 animate-pulse rounded-md bg-light-200 dark:bg-dark-200" />
|
<div className="h-6 w-6 animate-pulse rounded-[5px] bg-light-200 dark:bg-dark-200" />
|
||||||
<div
|
<div className="ml-2 h-6 w-[150px] animate-pulse rounded-[5px] bg-light-200 dark:bg-dark-200" />
|
||||||
className={twMerge(
|
|
||||||
"ml-2 h-6 w-[150px] animate-pulse rounded-md bg-light-200 dark:bg-dark-200",
|
|
||||||
isCollapsed && "hidden md:block",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Menu.Button
|
<Menu.Button className="mb-1 flex w-full items-center rounded-[5px] p-1.5 hover:bg-light-200 dark:hover:bg-dark-200">
|
||||||
className={twMerge(
|
<span className="inline-flex h-6 w-6 items-center justify-center rounded-[5px] bg-indigo-700">
|
||||||
"mb-1 flex h-[34px] w-full items-center rounded-md p-1.5 hover:bg-light-200 dark:hover:bg-dark-200",
|
|
||||||
isCollapsed && "md:mb-1.5 md:h-9 md:p-1",
|
|
||||||
)}
|
|
||||||
title={isCollapsed ? workspace.name : undefined}
|
|
||||||
>
|
|
||||||
<span className="inline-flex h-6 w-6 items-center justify-center rounded-md bg-indigo-700">
|
|
||||||
<span className="text-xs font-bold leading-none text-white">
|
<span className="text-xs font-bold leading-none text-white">
|
||||||
{workspace.name.charAt(0).toUpperCase()}
|
{workspace.name.charAt(0).toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span className="ml-2 text-sm font-bold text-neutral-900 dark:text-dark-1000">
|
||||||
className={twMerge(
|
|
||||||
"ml-2 text-sm font-bold text-neutral-900 dark:text-dark-1000",
|
|
||||||
isCollapsed && "md:hidden",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{workspace.name}
|
{workspace.name}
|
||||||
</span>
|
</span>
|
||||||
{workspace.plan === "pro" && (
|
{workspace.plan === "pro" && (
|
||||||
<span
|
<span className="ml-2 inline-flex items-center rounded-md bg-indigo-100 px-2 py-1 text-[10px] font-medium text-indigo-700">
|
||||||
className={twMerge(
|
|
||||||
"ml-2 inline-flex items-center rounded-md bg-indigo-100 px-2 py-1 text-[10px] font-medium text-indigo-700",
|
|
||||||
isCollapsed && "md:hidden",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Pro
|
Pro
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -73,12 +47,7 @@ export default function WorkspaceMenu({
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items
|
<Menu.Items className="absolute left-0 z-10 w-full origin-top-left rounded-md border border-light-600 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-600 dark:bg-dark-300">
|
||||||
className={twMerge(
|
|
||||||
"absolute left-0 z-10 origin-top-left rounded-md border border-light-600 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-600 dark:bg-dark-300",
|
|
||||||
isCollapsed ? "w-48" : "w-full",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
{availableWorkspaces.map((availableWorkspace) => (
|
{availableWorkspaces.map((availableWorkspace) => (
|
||||||
<div key={availableWorkspace.publicId} className="flex">
|
<div key={availableWorkspace.publicId} className="flex">
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ export const env = createEnv({
|
|||||||
S3_SECRET_ACCESS_KEY: z.string().optional(),
|
S3_SECRET_ACCESS_KEY: z.string().optional(),
|
||||||
S3_REGION: z.string().optional(),
|
S3_REGION: z.string().optional(),
|
||||||
S3_ENDPOINT: z.string().optional(),
|
S3_ENDPOINT: z.string().optional(),
|
||||||
S3_FORCE_PATH_STYLE: z.string().optional(),
|
|
||||||
EMAIL_FROM: z.string().optional(),
|
EMAIL_FROM: z.string().optional(),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import type { RefObject } from "react";
|
|
||||||
|
|
||||||
import { useEventListener } from "./useEventListener";
|
|
||||||
|
|
||||||
/** Supported event types. */
|
|
||||||
type EventType =
|
|
||||||
| "mousedown"
|
|
||||||
| "mouseup"
|
|
||||||
| "touchstart"
|
|
||||||
| "touchend"
|
|
||||||
| "focusin"
|
|
||||||
| "focusout";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom hook that handles clicks outside a specified element.
|
|
||||||
* @template T - The type of the element's reference.
|
|
||||||
* @param {RefObject<T> | RefObject<T>[]} ref - The React ref object(s) representing the element(s) to watch for outside clicks.
|
|
||||||
* @param {(event: MouseEvent | TouchEvent | FocusEvent) => void} handler - The callback function to be executed when a click outside the element occurs.
|
|
||||||
* @param {EventType} [eventType] - The mouse event type to listen for (optional, default is 'mousedown').
|
|
||||||
* @param {?AddEventListenerOptions} [eventListenerOptions] - The options object to be passed to the `addEventListener` method (optional).
|
|
||||||
* @returns {void}
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* const containerRef = useRef(null);
|
|
||||||
* useClickOutside([containerRef], () => {
|
|
||||||
* // Handle clicks outside the container.
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function useClickOutside<T extends HTMLElement = HTMLElement>(
|
|
||||||
ref: RefObject<T> | RefObject<T>[],
|
|
||||||
handler: (event: MouseEvent | TouchEvent | FocusEvent) => void,
|
|
||||||
eventType: EventType = "mousedown",
|
|
||||||
eventListenerOptions: AddEventListenerOptions = {},
|
|
||||||
): void {
|
|
||||||
useEventListener(
|
|
||||||
eventType,
|
|
||||||
(event) => {
|
|
||||||
const target = event.target as Node;
|
|
||||||
|
|
||||||
// Do nothing if the target is not connected element with document
|
|
||||||
if (!target.isConnected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isOutside = Array.isArray(ref)
|
|
||||||
? ref
|
|
||||||
.filter((r) => Boolean(r.current))
|
|
||||||
.every((r) => r.current && !r.current.contains(target))
|
|
||||||
: ref.current && !ref.current.contains(target);
|
|
||||||
|
|
||||||
if (isOutside) {
|
|
||||||
handler(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
eventListenerOptions,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import type { RefObject } from "react";
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
|
|
||||||
import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect";
|
|
||||||
|
|
||||||
// MediaQueryList Event based useEventListener interface
|
|
||||||
function useEventListener<K extends keyof MediaQueryListEventMap>(
|
|
||||||
eventName: K,
|
|
||||||
handler: (event: MediaQueryListEventMap[K]) => void,
|
|
||||||
element: RefObject<MediaQueryList>,
|
|
||||||
options?: boolean | AddEventListenerOptions,
|
|
||||||
): void;
|
|
||||||
|
|
||||||
// Window Event based useEventListener interface
|
|
||||||
function useEventListener<K extends keyof WindowEventMap>(
|
|
||||||
eventName: K,
|
|
||||||
handler: (event: WindowEventMap[K]) => void,
|
|
||||||
element?: undefined,
|
|
||||||
options?: boolean | AddEventListenerOptions,
|
|
||||||
): void;
|
|
||||||
|
|
||||||
// Element Event based useEventListener interface
|
|
||||||
function useEventListener<
|
|
||||||
K extends keyof HTMLElementEventMap & keyof SVGElementEventMap,
|
|
||||||
T extends Element = K extends keyof HTMLElementEventMap
|
|
||||||
? HTMLDivElement
|
|
||||||
: SVGElement,
|
|
||||||
>(
|
|
||||||
eventName: K,
|
|
||||||
handler:
|
|
||||||
| ((event: HTMLElementEventMap[K]) => void)
|
|
||||||
| ((event: SVGElementEventMap[K]) => void),
|
|
||||||
element: RefObject<T>,
|
|
||||||
options?: boolean | AddEventListenerOptions,
|
|
||||||
): void;
|
|
||||||
|
|
||||||
// Document Event based useEventListener interface
|
|
||||||
function useEventListener<K extends keyof DocumentEventMap>(
|
|
||||||
eventName: K,
|
|
||||||
handler: (event: DocumentEventMap[K]) => void,
|
|
||||||
element: RefObject<Document>,
|
|
||||||
options?: boolean | AddEventListenerOptions,
|
|
||||||
): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom hook that attaches event listeners to DOM elements, the window, or media query lists.
|
|
||||||
* @template KW - The type of event for window events.
|
|
||||||
* @template KH - The type of event for HTML or SVG element events.
|
|
||||||
* @template KM - The type of event for media query list events.
|
|
||||||
* @template T - The type of the DOM element (default is `HTMLElement`).
|
|
||||||
* @param {KW | KH | KM} eventName - The name of the event to listen for.
|
|
||||||
* @param {(event: WindowEventMap[KW] | HTMLElementEventMap[KH] | SVGElementEventMap[KH] | MediaQueryListEventMap[KM] | Event) => void} handler - The event handler function.
|
|
||||||
* @param {RefObject<T>} [element] - The DOM element or media query list to attach the event listener to (optional).
|
|
||||||
* @param {boolean | AddEventListenerOptions} [options] - An options object that specifies characteristics about the event listener (optional).
|
|
||||||
* @public
|
|
||||||
* @see [Documentation](https://usehooks-ts.com/react-hook/use-event-listener)
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* // Example 1: Attach a window event listener
|
|
||||||
* useEventListener('resize', handleResize);
|
|
||||||
* ```
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* // Example 2: Attach a document event listener with options
|
|
||||||
* const elementRef = useRef(document);
|
|
||||||
* useEventListener('click', handleClick, elementRef, { capture: true });
|
|
||||||
* ```
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* // Example 3: Attach an element event listener
|
|
||||||
* const buttonRef = useRef<HTMLButtonElement>(null);
|
|
||||||
* useEventListener('click', handleButtonClick, buttonRef);
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
function useEventListener<
|
|
||||||
KW extends keyof WindowEventMap,
|
|
||||||
KH extends keyof HTMLElementEventMap & keyof SVGElementEventMap,
|
|
||||||
KM extends keyof MediaQueryListEventMap,
|
|
||||||
T extends HTMLElement | SVGAElement | MediaQueryList = HTMLElement,
|
|
||||||
>(
|
|
||||||
eventName: KW | KH | KM,
|
|
||||||
handler: (
|
|
||||||
event:
|
|
||||||
| WindowEventMap[KW]
|
|
||||||
| HTMLElementEventMap[KH]
|
|
||||||
| SVGElementEventMap[KH]
|
|
||||||
| MediaQueryListEventMap[KM]
|
|
||||||
| Event,
|
|
||||||
) => void,
|
|
||||||
element?: RefObject<T>,
|
|
||||||
options?: boolean | AddEventListenerOptions,
|
|
||||||
) {
|
|
||||||
// Create a ref that stores handler
|
|
||||||
const savedHandler = useRef(handler);
|
|
||||||
|
|
||||||
useIsomorphicLayoutEffect(() => {
|
|
||||||
savedHandler.current = handler;
|
|
||||||
}, [handler]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Define the listening target
|
|
||||||
const targetElement: T | Window = element?.current ?? window;
|
|
||||||
|
|
||||||
if (!(targetElement && targetElement.addEventListener)) return;
|
|
||||||
|
|
||||||
// Create event listener that calls handler function stored in ref
|
|
||||||
const listener: typeof handler = (event) => {
|
|
||||||
savedHandler.current(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
targetElement.addEventListener(eventName, listener, options);
|
|
||||||
|
|
||||||
// Remove event listener on cleanup
|
|
||||||
return () => {
|
|
||||||
targetElement.removeEventListener(eventName, listener, options);
|
|
||||||
};
|
|
||||||
}, [eventName, element, options]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { useEventListener };
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { useEffect, useLayoutEffect } from "react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom hook that uses either `useLayoutEffect` or `useEffect` based on the environment (client-side or server-side).
|
|
||||||
* @param {Function} effect - The effect function to be executed.
|
|
||||||
* @param {Array<any>} [dependencies] - An array of dependencies for the effect (optional).
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* useIsomorphicLayoutEffect(() => {
|
|
||||||
* // Code to be executed during the layout phase on the client side
|
|
||||||
* }, [dependency1, dependency2]);
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const useIsomorphicLayoutEffect =
|
|
||||||
typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export function useMediaQuery(query: string): boolean {
|
|
||||||
const [matches, setMatches] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const media = window.matchMedia(query);
|
|
||||||
setMatches(media.matches);
|
|
||||||
|
|
||||||
const listener = (event: MediaQueryListEvent) => {
|
|
||||||
setMatches(event.matches);
|
|
||||||
};
|
|
||||||
|
|
||||||
media.addEventListener("change", listener);
|
|
||||||
return () => media.removeEventListener("change", listener);
|
|
||||||
}, [query]);
|
|
||||||
|
|
||||||
return matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useIsMobile(): boolean {
|
|
||||||
return useMediaQuery("(max-width: 767px)");
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { NextRequest } from "next/server";
|
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
import { env } from "next-runtime-env";
|
|
||||||
|
|
||||||
export function middleware(request: NextRequest) {
|
|
||||||
if (request.nextUrl.pathname === "/") {
|
|
||||||
if (env("NEXT_PUBLIC_KAN_ENV") !== "cloud") {
|
|
||||||
const loginUrl = new URL("/login", request.url);
|
|
||||||
return NextResponse.redirect(loginUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
matcher: ["/"],
|
|
||||||
};
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import "~/styles/globals.css";
|
import "~/styles/globals.css";
|
||||||
import "~/utils/i18n";
|
import "~/utils/i18n";
|
||||||
|
|
||||||
import type { NextPage, Viewport } from "next";
|
import type { AppType } from "next/app";
|
||||||
import type { AppProps, AppType } from "next/app";
|
|
||||||
import type { ReactElement, ReactNode } from "react";
|
|
||||||
import { Plus_Jakarta_Sans } from "next/font/google";
|
import { Plus_Jakarta_Sans } from "next/font/google";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import { env } from "next-runtime-env";
|
import { env } from "next-runtime-env";
|
||||||
@@ -28,22 +26,7 @@ export const metadata = {
|
|||||||
icons: [{ rel: "icon", url: "/favicon.ico" }],
|
icons: [{ rel: "icon", url: "/favicon.ico" }],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const viewport: Viewport = {
|
const MyApp: AppType = ({ Component, pageProps }) => {
|
||||||
width: "device-width",
|
|
||||||
initialScale: 1,
|
|
||||||
maximumScale: 1,
|
|
||||||
userScalable: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
|
||||||
getLayout?: (page: ReactElement) => ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AppPropsWithLayout = AppProps & {
|
|
||||||
Component: NextPageWithLayout;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MyApp: AppType = ({ Component, pageProps }: AppPropsWithLayout) => {
|
|
||||||
const posthogKey = env("NEXT_PUBLIC_POSTHOG_KEY");
|
const posthogKey = env("NEXT_PUBLIC_POSTHOG_KEY");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -59,8 +42,6 @@ const MyApp: AppType = ({ Component, pageProps }: AppPropsWithLayout) => {
|
|||||||
}
|
}
|
||||||
}, [posthogKey]);
|
}, [posthogKey]);
|
||||||
|
|
||||||
const getLayout = Component.getLayout ?? ((page) => page);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
@@ -86,10 +67,10 @@ const MyApp: AppType = ({ Component, pageProps }: AppPropsWithLayout) => {
|
|||||||
<PopupProvider>
|
<PopupProvider>
|
||||||
{posthogKey ? (
|
{posthogKey ? (
|
||||||
<PostHogProvider client={posthog}>
|
<PostHogProvider client={posthog}>
|
||||||
{getLayout(<Component {...pageProps} />)}
|
<Component {...pageProps} />
|
||||||
</PostHogProvider>
|
</PostHogProvider>
|
||||||
) : (
|
) : (
|
||||||
getLayout(<Component {...pageProps} />)
|
<Component {...pageProps} />
|
||||||
)}
|
)}
|
||||||
</PopupProvider>
|
</PopupProvider>
|
||||||
</ModalProvider>
|
</ModalProvider>
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ export default async function handler(
|
|||||||
const client = new S3Client({
|
const client = new S3Client({
|
||||||
region: env.S3_REGION ?? "",
|
region: env.S3_REGION ?? "",
|
||||||
endpoint: env.S3_ENDPOINT ?? "",
|
endpoint: env.S3_ENDPOINT ?? "",
|
||||||
forcePathStyle: env.S3_FORCE_PATH_STYLE === "true",
|
|
||||||
credentials: {
|
credentials: {
|
||||||
accessKeyId: env.S3_ACCESS_KEY_ID ?? "",
|
accessKeyId: env.S3_ACCESS_KEY_ID ?? "",
|
||||||
secretAccessKey: env.S3_SECRET_ACCESS_KEY ?? "",
|
secretAccessKey: env.S3_SECRET_ACCESS_KEY ?? "",
|
||||||
@@ -59,7 +58,6 @@ export default async function handler(
|
|||||||
new PutObjectCommand({
|
new PutObjectCommand({
|
||||||
Bucket: nextRuntimeEnv("NEXT_PUBLIC_AVATAR_BUCKET_NAME") ?? "",
|
Bucket: nextRuntimeEnv("NEXT_PUBLIC_AVATAR_BUCKET_NAME") ?? "",
|
||||||
Key: filename,
|
Key: filename,
|
||||||
ACL: "public-read",
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||||
|
|
||||||
import { openApiDocument } from "@kan/api/openapi";
|
import { openApiDocument } from "@kan/api/openapi";
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import type { NextPageWithLayout } from "~/pages/_app";
|
import { WorkspaceProvider } from "~/providers/workspace";
|
||||||
import { getDashboardLayout } from "~/components/Dashboard";
|
import Dashboard from "~/components/Dashboard";
|
||||||
import Popup from "~/components/Popup";
|
import Popup from "~/components/Popup";
|
||||||
import BoardView from "~/views/board";
|
import BoardView from "~/views/board";
|
||||||
|
|
||||||
const BoardPage: NextPageWithLayout = () => {
|
export default function BoardPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<WorkspaceProvider>
|
||||||
<BoardView />
|
<Dashboard>
|
||||||
|
<BoardView />
|
||||||
|
</Dashboard>
|
||||||
<Popup />
|
<Popup />
|
||||||
</>
|
</WorkspaceProvider>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
BoardPage.getLayout = (page) => getDashboardLayout(page);
|
|
||||||
|
|
||||||
export default BoardPage;
|
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import type { NextPageWithLayout } from "../_app";
|
import { WorkspaceProvider } from "~/providers/workspace";
|
||||||
import { getDashboardLayout } from "~/components/Dashboard";
|
import Dashboard from "~/components/Dashboard";
|
||||||
import Popup from "~/components/Popup";
|
import Popup from "~/components/Popup";
|
||||||
import BoardsView from "~/views/boards";
|
import BoardsView from "~/views/boards";
|
||||||
|
|
||||||
const BoardsPage: NextPageWithLayout = () => {
|
export default function BoardsPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<WorkspaceProvider>
|
||||||
<BoardsView />
|
<Dashboard>
|
||||||
|
<BoardsView />
|
||||||
|
</Dashboard>
|
||||||
<Popup />
|
<Popup />
|
||||||
</>
|
</WorkspaceProvider>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
BoardsPage.getLayout = (page) => getDashboardLayout(page);
|
|
||||||
|
|
||||||
export default BoardsPage;
|
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import type { NextPageWithLayout } from "~/pages/_app";
|
import { WorkspaceProvider } from "~/providers/workspace";
|
||||||
import { getDashboardLayout } from "~/components/Dashboard";
|
import Dashboard from "~/components/Dashboard";
|
||||||
import Popup from "~/components/Popup";
|
import Popup from "~/components/Popup";
|
||||||
import CardView, { CardRightPanel } from "~/views/card";
|
import CardView from "~/views/card";
|
||||||
|
|
||||||
const CardPage: NextPageWithLayout = () => {
|
export default function CardPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<WorkspaceProvider>
|
||||||
<CardView />
|
<Dashboard>
|
||||||
|
<CardView />
|
||||||
|
</Dashboard>
|
||||||
<Popup />
|
<Popup />
|
||||||
</>
|
</WorkspaceProvider>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
CardPage.getLayout = (page) =>
|
|
||||||
getDashboardLayout(page, <CardRightPanel />, true);
|
|
||||||
|
|
||||||
export default CardPage;
|
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import type { NextPageWithLayout } from "~/pages/_app";
|
import { WorkspaceProvider } from "~/providers/workspace";
|
||||||
import { getDashboardLayout } from "~/components/Dashboard";
|
import Dashboard from "~/components/Dashboard";
|
||||||
import Popup from "~/components/Popup";
|
import Popup from "~/components/Popup";
|
||||||
import MembersView from "~/views/members";
|
import MembersView from "~/views/members";
|
||||||
|
|
||||||
const MembersPage: NextPageWithLayout = () => {
|
export default function MembersPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<WorkspaceProvider>
|
||||||
<MembersView />
|
<Dashboard>
|
||||||
|
<MembersView />
|
||||||
|
</Dashboard>
|
||||||
<Popup />
|
<Popup />
|
||||||
</>
|
</WorkspaceProvider>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
MembersPage.getLayout = (page) => getDashboardLayout(page);
|
|
||||||
|
|
||||||
export default MembersPage;
|
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import type { NextPageWithLayout } from "~/pages/_app";
|
import { WorkspaceProvider } from "~/providers/workspace";
|
||||||
import { getDashboardLayout } from "~/components/Dashboard";
|
import Dashboard from "~/components/Dashboard";
|
||||||
import Popup from "~/components/Popup";
|
|
||||||
import SettingsView from "~/views/settings";
|
import SettingsView from "~/views/settings";
|
||||||
|
import Popup from "~/components/Popup";
|
||||||
|
|
||||||
const SettingsPage: NextPageWithLayout = () => {
|
export default function SettingsPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<WorkspaceProvider>
|
||||||
<SettingsView />
|
<Dashboard>
|
||||||
|
<SettingsView />
|
||||||
|
</Dashboard>
|
||||||
<Popup />
|
<Popup />
|
||||||
</>
|
</WorkspaceProvider>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
SettingsPage.getLayout = (page) => getDashboardLayout(page);
|
|
||||||
|
|
||||||
export default SettingsPage;
|
|
||||||
|
|||||||
@@ -2,11 +2,10 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
[contenteditable="true"]:empty:before {
|
[contenteditable=true]:empty:before {
|
||||||
content: attr(placeholder);
|
content: attr(placeholder);
|
||||||
display: block;
|
display: block;
|
||||||
color: #707070;
|
color: #707070;
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gradient-border::before {
|
.gradient-border::before {
|
||||||
@@ -19,7 +18,7 @@
|
|||||||
transparent 110deg,
|
transparent 110deg,
|
||||||
transparent 360deg
|
transparent 360deg
|
||||||
);
|
);
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -50%;
|
inset: -50%;
|
||||||
width: 200%;
|
width: 200%;
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { env } from "next-runtime-env";
|
|
||||||
import { HiLink } from "react-icons/hi";
|
|
||||||
|
|
||||||
const UpdateBoardSlugButton = ({
|
|
||||||
handleOnClick,
|
|
||||||
workspaceSlug,
|
|
||||||
boardSlug,
|
|
||||||
isLoading,
|
|
||||||
}: {
|
|
||||||
handleOnClick: () => void;
|
|
||||||
workspaceSlug: string;
|
|
||||||
boardSlug: string;
|
|
||||||
isLoading: boolean;
|
|
||||||
}) => {
|
|
||||||
if (!isLoading && (!workspaceSlug || !boardSlug)) return <></>;
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="hidden h-[36px] w-[225px] animate-pulse rounded-full bg-light-200 dark:bg-dark-100 xl:flex" />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={handleOnClick}
|
|
||||||
className="hidden cursor-pointer items-center gap-2 rounded-full border-[1px] bg-light-50 p-1 pl-4 pr-1 text-sm text-light-950 hover:bg-light-100 dark:border-dark-600 dark:bg-dark-50 dark:text-dark-900 dark:hover:bg-dark-100 xl:flex"
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span>
|
|
||||||
{env("NEXT_PUBLIC_KAN_ENV") === "cloud"
|
|
||||||
? "kan.bn"
|
|
||||||
: env("NEXT_PUBLIC_BASE_URL")}
|
|
||||||
</span>
|
|
||||||
<div className="mx-1.5 h-4 w-px rotate-[20deg] bg-gray-300 dark:bg-dark-600"></div>
|
|
||||||
<span>{workspaceSlug}</span>
|
|
||||||
<div className="mx-1.5 h-4 w-px rotate-[20deg] bg-gray-300 dark:bg-dark-600"></div>
|
|
||||||
<span>{boardSlug}</span>
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
href={`${env("NEXT_PUBLIC_BASE_URL")}/${workspaceSlug}/${boardSlug}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-light-200 dark:hover:bg-dark-200"
|
|
||||||
>
|
|
||||||
<HiLink className="h-[13px] w-[13px]" />
|
|
||||||
</Link>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UpdateBoardSlugButton;
|
|
||||||
@@ -135,7 +135,7 @@ export function UpdateBoardSlugForm({
|
|||||||
? t`This board URL has already been taken`
|
? t`This board URL has already been taken`
|
||||||
: undefined)
|
: undefined)
|
||||||
}
|
}
|
||||||
prefix={`${env("NEXT_PUBLIC_KAN_ENV") === "cloud" ? "kan.bn" : env("NEXT_PUBLIC_BASE_URL")}/${workspaceSlug}/`}
|
prefix={`${env("NEXT_PUBLIC_BASE_URL")}/${workspaceSlug}/`}
|
||||||
onKeyDown={async (e) => {
|
onKeyDown={async (e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -152,14 +152,7 @@ export function UpdateBoardSlugForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||||
<div className="flex items-center gap-2">
|
<div>
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
href="/settings?edit=workspace_url"
|
|
||||||
onClick={closeModal}
|
|
||||||
>
|
|
||||||
{t`Edit workspace URL`}
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
isLoading={updateBoardSlug.isPending}
|
isLoading={updateBoardSlug.isPending}
|
||||||
@@ -167,8 +160,7 @@ export function UpdateBoardSlugForm({
|
|||||||
!isDirty ||
|
!isDirty ||
|
||||||
updateBoardSlug.isPending ||
|
updateBoardSlug.isPending ||
|
||||||
errors.slug?.message !== undefined ||
|
errors.slug?.message !== undefined ||
|
||||||
isBoardSlugAvailable?.isReserved ||
|
isBoardSlugAvailable?.isReserved
|
||||||
checkBoardSlugAvailability.isLoading
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t`Update`}
|
{t`Update`}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import type { UpdateBoardInput } from "@kan/api/types";
|
|||||||
|
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import { DeleteLabelConfirmation } from "~/components/DeleteLabelConfirmation";
|
import { DeleteLabelConfirmation } from "~/components/DeleteLabelConfirmation";
|
||||||
import FeedbackModal from "~/components/FeedbackModal";
|
|
||||||
import { LabelForm } from "~/components/LabelForm";
|
import { LabelForm } from "~/components/LabelForm";
|
||||||
import Modal from "~/components/modal";
|
import Modal from "~/components/modal";
|
||||||
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||||
@@ -33,7 +32,6 @@ import Filters from "./components/Filters";
|
|||||||
import List from "./components/List";
|
import List from "./components/List";
|
||||||
import { NewCardForm } from "./components/NewCardForm";
|
import { NewCardForm } from "./components/NewCardForm";
|
||||||
import { NewListForm } from "./components/NewListForm";
|
import { NewListForm } from "./components/NewListForm";
|
||||||
import UpdateBoardSlugButton from "./components/UpdateBoardSlugButton";
|
|
||||||
import { UpdateBoardSlugForm } from "./components/UpdateBoardSlugForm";
|
import { UpdateBoardSlugForm } from "./components/UpdateBoardSlugForm";
|
||||||
import VisibilityButton from "./components/VisibilityButton";
|
import VisibilityButton from "./components/VisibilityButton";
|
||||||
|
|
||||||
@@ -242,7 +240,7 @@ export default function BoardPage() {
|
|||||||
/>
|
/>
|
||||||
<div className="relative flex h-full flex-col">
|
<div className="relative flex h-full flex-col">
|
||||||
<PatternedBackground />
|
<PatternedBackground />
|
||||||
<div className="z-10 flex w-full flex-col justify-between p-6 md:flex-row md:p-8">
|
<div className="z-10 flex w-full justify-between p-8">
|
||||||
{isLoading && !boardData && (
|
{isLoading && !boardData && (
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<div className="h-[2.3rem] w-[150px] animate-pulse rounded-[5px] bg-light-200 dark:bg-dark-100" />
|
<div className="h-[2.3rem] w-[150px] animate-pulse rounded-[5px] bg-light-200 dark:bg-dark-100" />
|
||||||
@@ -251,7 +249,7 @@ export default function BoardPage() {
|
|||||||
{boardData && (
|
{boardData && (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="order-2 focus-visible:outline-none md:order-1"
|
className="focus-visible:outline-none"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
id="name"
|
id="name"
|
||||||
@@ -263,18 +261,12 @@ export default function BoardPage() {
|
|||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
{!boardData && !isLoading && (
|
{!boardData && !isLoading && (
|
||||||
<p className="order-2 block p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem] md:order-1">
|
<p className="block p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
|
||||||
{t`Board not found`}
|
{t`Board not found`}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="order-1 mb-4 flex items-center justify-end space-x-2 md:order-2 md:mb-0">
|
<div className="flex items-center space-x-2">
|
||||||
<UpdateBoardSlugButton
|
|
||||||
handleOnClick={() => openModal("UPDATE_BOARD_SLUG")}
|
|
||||||
isLoading={isLoading}
|
|
||||||
workspaceSlug={workspace.slug ?? ""}
|
|
||||||
boardSlug={boardData?.slug ?? ""}
|
|
||||||
/>
|
|
||||||
<VisibilityButton
|
<VisibilityButton
|
||||||
visibility={boardData?.visibility ?? "private"}
|
visibility={boardData?.visibility ?? "private"}
|
||||||
boardPublicId={boardId ?? ""}
|
boardPublicId={boardId ?? ""}
|
||||||
@@ -366,7 +358,7 @@ export default function BoardPage() {
|
|||||||
<div
|
<div
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
className="scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-w-[8px] z-10 h-full max-h-[calc(100vh-225px)] min-h-[2rem] overflow-y-auto pr-1 scrollbar dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-600"
|
className="scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-w-[8px] z-10 h-full max-h-[calc(100vh-265px)] min-h-[2rem] overflow-y-auto pr-1 scrollbar dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-600"
|
||||||
>
|
>
|
||||||
{list.cards.map((card, index) => (
|
{list.cards.map((card, index) => (
|
||||||
<Draggable
|
<Draggable
|
||||||
@@ -422,15 +414,7 @@ export default function BoardPage() {
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<Modal
|
<Modal modalSize={modalContentType === "NEW_CARD" ? "md" : "sm"}>
|
||||||
modalSize={
|
|
||||||
modalContentType === "NEW_CARD" ||
|
|
||||||
modalContentType === "NEW_FEEDBACK"
|
|
||||||
? "md"
|
|
||||||
: "sm"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{modalContentType === "NEW_FEEDBACK" && <FeedbackModal />}
|
|
||||||
{modalContentType === "DELETE_BOARD" && (
|
{modalContentType === "DELETE_BOARD" && (
|
||||||
<DeleteBoardConfirmation boardPublicId={boardId ?? ""} />
|
<DeleteBoardConfirmation boardPublicId={boardId ?? ""} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function BoardsList() {
|
|||||||
|
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
return (
|
return (
|
||||||
<div className="grid h-fit w-full grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-7">
|
<div className="xxl:grid-cols-7 grid w-full grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
<div className="mr-5 flex h-[150px] w-full animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
|
<div className="mr-5 flex h-[150px] w-full animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
|
||||||
<div className="mr-5 flex h-[150px] w-full animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
|
<div className="mr-5 flex h-[150px] w-full animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
|
||||||
<div className="mr-5 flex h-[150px] w-full animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
|
<div className="mr-5 flex h-[150px] w-full animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
|
||||||
@@ -45,12 +45,12 @@ export function BoardsList() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid h-fit w-full grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-7">
|
<div className="grid h-fit w-full grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-7">
|
||||||
{data?.map((board) => (
|
{data?.map((board) => (
|
||||||
<Link key={board.publicId} href={`boards/${board.publicId}`}>
|
<Link key={board.publicId} href={`boards/${board.publicId}`}>
|
||||||
<div className="align-center relative mr-5 flex h-[150px] w-full items-center justify-center rounded-md border border-dashed border-light-400 bg-light-50 shadow-sm hover:bg-light-200 dark:border-dark-600 dark:bg-dark-50 dark:hover:bg-dark-100">
|
<div className="align-center relative mr-5 flex h-[150px] w-full items-center justify-center rounded-md border border-dashed border-light-400 bg-light-50 shadow-sm hover:bg-light-200 dark:border-dark-600 dark:bg-dark-50 dark:hover:bg-dark-100">
|
||||||
<PatternedBackground />
|
<PatternedBackground />
|
||||||
<p className="px-4 text-[14px] font-bold text-neutral-700 dark:text-dark-1000">
|
<p className="text-md px-4 font-bold text-neutral-900 dark:text-dark-1000">
|
||||||
{board.name}
|
{board.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ const SelectSource = ({ handleNextStep }: { handleNextStep: () => void }) => {
|
|||||||
<Listbox.Button className="focus-ring-light-700 block w-full rounded-md border-0 bg-dark-300 bg-white/5 px-4 py-1.5 text-neutral-900 shadow-sm ring-1 ring-inset ring-light-600 focus:ring-2 focus:ring-inset dark:text-dark-1000 dark:ring-dark-700 dark:focus:ring-dark-700 sm:text-sm sm:leading-6">
|
<Listbox.Button className="focus-ring-light-700 block w-full rounded-md border-0 bg-dark-300 bg-white/5 px-4 py-1.5 text-neutral-900 shadow-sm ring-1 ring-inset ring-light-600 focus:ring-2 focus:ring-inset dark:text-dark-1000 dark:ring-dark-700 dark:focus:ring-dark-700 sm:text-sm sm:leading-6">
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
{integrationProviders[field.value]?.icon}
|
{integrationProviders[field.value]?.icon}
|
||||||
<span className="ml-2 block truncate text-sm">
|
<span className="ml-2 block truncate">
|
||||||
{integrationProviders[field.value]?.name}
|
{integrationProviders[field.value]?.name}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -113,7 +113,7 @@ const SelectSource = ({ handleNextStep }: { handleNextStep: () => void }) => {
|
|||||||
integrationProviders[integration.provider]
|
integrationProviders[integration.provider]
|
||||||
?.icon
|
?.icon
|
||||||
}
|
}
|
||||||
<span className="ml-2 block truncate text-sm font-normal">
|
<span className="ml-2 block truncate font-normal">
|
||||||
{
|
{
|
||||||
integrationProviders[integration.provider]
|
integrationProviders[integration.provider]
|
||||||
?.name
|
?.name
|
||||||
@@ -125,12 +125,12 @@ const SelectSource = ({ handleNextStep }: { handleNextStep: () => void }) => {
|
|||||||
) : (
|
) : (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key="trello_placeholder"
|
key="trello_placeholder"
|
||||||
className="font-sm relative cursor-default select-none px-1"
|
className="relative cursor-default select-none px-1"
|
||||||
value="trello"
|
value="trello"
|
||||||
>
|
>
|
||||||
<div className="flex items-center rounded-[5px] p-1 text-sm hover:bg-light-200 dark:hover:bg-dark-400">
|
<div className="flex items-center rounded-[5px] p-1 hover:bg-light-200 dark:hover:bg-dark-400">
|
||||||
{integrationProviders.trello?.icon}
|
{integrationProviders.trello?.icon}
|
||||||
<span className="ml-2 block truncate text-sm">
|
<span className="ml-2 block truncate font-normal">
|
||||||
{integrationProviders.trello?.name}
|
{integrationProviders.trello?.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,59 +1,28 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { HiXMark } from "react-icons/hi2";
|
import { HiXMark } from "react-icons/hi2";
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import type { Template } from "./TemplateBoards";
|
import type { NewBoardInput } from "@kan/api/types";
|
||||||
|
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import Input from "~/components/Input";
|
import Input from "~/components/Input";
|
||||||
import Toggle from "~/components/Toggle";
|
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { useWorkspace } from "~/providers/workspace";
|
import { useWorkspace } from "~/providers/workspace";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import TemplateBoards, { getTemplates } from "./TemplateBoards";
|
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
name: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: t`Board name is required` })
|
|
||||||
.max(100, { message: t`Board name cannot exceed 100 characters` }),
|
|
||||||
workspacePublicId: z.string(),
|
|
||||||
template: z.custom<Template | null>(),
|
|
||||||
});
|
|
||||||
|
|
||||||
interface NewBoardInputWithTemplate {
|
|
||||||
name: string;
|
|
||||||
workspacePublicId: string;
|
|
||||||
template: Template | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NewBoardForm() {
|
export function NewBoardForm() {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { closeModal } = useModal();
|
const { closeModal } = useModal();
|
||||||
const { workspace } = useWorkspace();
|
const { workspace } = useWorkspace();
|
||||||
const [showTemplates, setShowTemplates] = useState(false);
|
|
||||||
|
|
||||||
const templates = getTemplates();
|
const { register, handleSubmit } = useForm<NewBoardInput>({
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
watch,
|
|
||||||
setValue,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<NewBoardInputWithTemplate>({
|
|
||||||
resolver: zodResolver(schema),
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
workspacePublicId: workspace.publicId || "",
|
workspacePublicId: workspace.publicId || "",
|
||||||
template: null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentTemplate = watch("template");
|
|
||||||
|
|
||||||
const refetchBoards = () => utils.board.all.refetch();
|
const refetchBoards = () => utils.board.all.refetch();
|
||||||
|
|
||||||
const createBoard = api.board.create.useMutation({
|
const createBoard = api.board.create.useMutation({
|
||||||
@@ -63,13 +32,8 @@ export function NewBoardForm() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (data: NewBoardInputWithTemplate) => {
|
const onSubmit = (data: NewBoardInput) => {
|
||||||
createBoard.mutate({
|
createBoard.mutate(data);
|
||||||
name: data.name,
|
|
||||||
workspacePublicId: data.workspacePublicId,
|
|
||||||
lists: data.template?.lists ?? [],
|
|
||||||
labels: data.template?.labels ?? [],
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -98,7 +62,6 @@ export function NewBoardForm() {
|
|||||||
id="name"
|
id="name"
|
||||||
placeholder={t`Name`}
|
placeholder={t`Name`}
|
||||||
{...register("name", { required: true })}
|
{...register("name", { required: true })}
|
||||||
errorMessage={errors.name?.message}
|
|
||||||
onKeyDown={async (e) => {
|
onKeyDown={async (e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -107,22 +70,8 @@ export function NewBoardForm() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TemplateBoards
|
|
||||||
currentBoard={currentTemplate}
|
|
||||||
setCurrentBoard={(t) => setValue("template", t)}
|
|
||||||
showTemplates={showTemplates}
|
|
||||||
/>
|
|
||||||
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||||
<Toggle
|
|
||||||
label={t`Use template`}
|
|
||||||
isChecked={showTemplates}
|
|
||||||
onChange={() => {
|
|
||||||
setShowTemplates(!showTemplates);
|
|
||||||
if (!showTemplates && !currentTemplate) {
|
|
||||||
setValue("template", templates[0] ?? null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<Button type="submit" isLoading={createBoard.isPending}>
|
<Button type="submit" isLoading={createBoard.isPending}>
|
||||||
{t`Create board`}
|
{t`Create board`}
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
import { t } from "@lingui/core/macro";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { HiCheckCircle } from "react-icons/hi2";
|
|
||||||
|
|
||||||
export interface Template {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
lists: string[];
|
|
||||||
labels: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getTemplates = (): Template[] => [
|
|
||||||
{
|
|
||||||
id: "basic",
|
|
||||||
name: t`Basic Kanban`,
|
|
||||||
lists: [t`To Do`, t`In Progress`, t`Done`],
|
|
||||||
labels: [t`High Priority`, t`Medium Priority`, t`Low Priority`],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "software-dev",
|
|
||||||
name: t`Software Development`,
|
|
||||||
lists: [t`Backlog`, t`To Do`, t`In Progress`, t`Code Review`, t`Done`],
|
|
||||||
labels: [t`Bug`, t`Feature`, t`Enhancement`, t`Critical`, t`Documentation`],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "content-creation",
|
|
||||||
name: t`Content Creation`,
|
|
||||||
lists: [
|
|
||||||
t`Brainstorming`,
|
|
||||||
t`Writing`,
|
|
||||||
t`Editing`,
|
|
||||||
t`Design`,
|
|
||||||
t`Approval`,
|
|
||||||
t`Publishing`,
|
|
||||||
t`Done`,
|
|
||||||
],
|
|
||||||
labels: [t`Blog Post`, t`Social Media`, t`Video`, t`Newsletter`, t`Urgent`],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "customer-support",
|
|
||||||
name: t`Customer Support`,
|
|
||||||
lists: [
|
|
||||||
t`New Ticket`,
|
|
||||||
t`Triaging`,
|
|
||||||
t`In Progress`,
|
|
||||||
t`Awaiting Customer`,
|
|
||||||
t`Resolution`,
|
|
||||||
t`Done`,
|
|
||||||
],
|
|
||||||
labels: [
|
|
||||||
t`Bug Report`,
|
|
||||||
t`Feature Request`,
|
|
||||||
t`Question`,
|
|
||||||
t`Urgent`,
|
|
||||||
t`Billing`,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "recruitment",
|
|
||||||
name: t`Recruitment`,
|
|
||||||
lists: [
|
|
||||||
t`Applicants`,
|
|
||||||
t`Screening`,
|
|
||||||
t`Interviewing`,
|
|
||||||
t`Offer`,
|
|
||||||
t`Onboarding`,
|
|
||||||
t`Hired`,
|
|
||||||
],
|
|
||||||
labels: [t`Remote`, t`Full-time`, t`Part-time`, t`Senior`, t`Junior`],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "personal-project",
|
|
||||||
name: t`Personal Project`,
|
|
||||||
lists: [
|
|
||||||
t`Ideas`,
|
|
||||||
t`Research`,
|
|
||||||
t`Planning`,
|
|
||||||
t`Execution`,
|
|
||||||
t`Review`,
|
|
||||||
t`Next Steps`,
|
|
||||||
t`Complete`,
|
|
||||||
],
|
|
||||||
labels: [t`Important`, t`Quick Win`, t`Long-term`, t`Learning`, t`Fun`],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function TemplateBoards({
|
|
||||||
currentBoard,
|
|
||||||
setCurrentBoard,
|
|
||||||
showTemplates,
|
|
||||||
}: {
|
|
||||||
currentBoard: Template | null;
|
|
||||||
setCurrentBoard: (board: Template | null) => void;
|
|
||||||
showTemplates: boolean;
|
|
||||||
}) {
|
|
||||||
const [showFade, setShowFade] = useState(false);
|
|
||||||
const [showTopFade, setShowTopFade] = useState(false);
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const templates = getTemplates();
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (!scrollRef.current) return;
|
|
||||||
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
|
||||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 5;
|
|
||||||
const isAtTop = scrollTop <= 5;
|
|
||||||
|
|
||||||
setShowFade(!isAtBottom);
|
|
||||||
setShowTopFade(!isAtTop);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const scrollElement = scrollRef.current;
|
|
||||||
if (!scrollElement) return;
|
|
||||||
|
|
||||||
handleScroll();
|
|
||||||
|
|
||||||
scrollElement.addEventListener("scroll", handleScroll);
|
|
||||||
return () => scrollElement.removeEventListener("scroll", handleScroll);
|
|
||||||
}, [showTemplates]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (showTemplates && currentBoard && scrollRef.current) {
|
|
||||||
const selectedElement = scrollRef.current.querySelector(
|
|
||||||
`[data-template-id="${currentBoard.id}"]`,
|
|
||||||
);
|
|
||||||
if (selectedElement) {
|
|
||||||
selectedElement.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [showTemplates, currentBoard]);
|
|
||||||
|
|
||||||
const handleBoardSelect = (boardId: string) => {
|
|
||||||
if (currentBoard?.id === boardId) {
|
|
||||||
setCurrentBoard(null);
|
|
||||||
} else {
|
|
||||||
setCurrentBoard(
|
|
||||||
templates.find((template) => template.id === boardId) ?? null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!showTemplates) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="px-5 pt-4">
|
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
ref={scrollRef}
|
|
||||||
className="scroll-container -mr-2 flex max-h-[200px] flex-col gap-3 overflow-y-auto pr-2 pt-0.5"
|
|
||||||
>
|
|
||||||
{templates.map((template) => (
|
|
||||||
<label
|
|
||||||
key={template.id}
|
|
||||||
data-template-id={template.id}
|
|
||||||
onClick={() => handleBoardSelect(template.id)}
|
|
||||||
className={`scroll-container relative flex cursor-pointer rounded-lg border p-3 transition-all hover:bg-light-100 dark:hover:bg-dark-200 ${
|
|
||||||
currentBoard?.id === template.id
|
|
||||||
? "border-light-700 bg-light-100 ring-1 ring-inset ring-light-700 dark:border-dark-700 dark:bg-dark-200 dark:ring-dark-700"
|
|
||||||
: "border-light-600 dark:border-dark-600"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
||||||
{template.name}
|
|
||||||
</h4>
|
|
||||||
<p className="text-xs text-light-950 dark:text-dark-900">
|
|
||||||
{template.lists.join(", ")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{currentBoard?.id === template.id && (
|
|
||||||
<div className="absolute right-3 top-3 text-light-1000 dark:text-dark-1000">
|
|
||||||
<HiCheckCircle className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{showTopFade && (
|
|
||||||
<div className="pointer-events-none absolute left-0 right-0 top-0 h-6 bg-gradient-to-b from-white/80 to-transparent dark:from-dark-100/80" />
|
|
||||||
)}
|
|
||||||
{showFade && (
|
|
||||||
<div className="pointer-events-none absolute bottom-0 left-0 right-0 h-6 bg-gradient-to-t from-white/80 to-transparent dark:from-dark-100/80" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { HiArrowDownTray, HiOutlinePlusSmall } from "react-icons/hi2";
|
import { HiArrowDownTray, HiOutlinePlusSmall } from "react-icons/hi2";
|
||||||
|
|
||||||
import Button from "~/components/Button";
|
|
||||||
import FeedbackModal from "~/components/FeedbackModal";
|
|
||||||
import Modal from "~/components/modal";
|
import Modal from "~/components/modal";
|
||||||
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||||
import { PageHead } from "~/components/PageHead";
|
import { PageHead } from "~/components/PageHead";
|
||||||
@@ -21,37 +19,42 @@ export default function BoardsPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title={t`Boards | ${workspace.name ?? "Workspace"}`} />
|
<PageHead title={t`Boards | ${workspace.name ?? "Workspace"}`} />
|
||||||
<div className="m-auto h-full max-w-[1100px] p-6 px-5 md:px-28 md:py-12">
|
<div className="h-full p-8">
|
||||||
<div className="relative z-10 mb-8 flex w-full items-center justify-between">
|
<div className="mb-8 flex w-full justify-between">
|
||||||
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
|
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
|
||||||
{t`Boards`}
|
{t`Boards`}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex gap-2">
|
<div className="flex">
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
className="bg-dark-3000 mr-2 flex items-center gap-x-1.5 rounded-md border-[1px] border-light-600 px-3 py-2 text-sm text-neutral-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 dark:border-dark-600 dark:text-dark-1000"
|
||||||
onClick={() => openModal("IMPORT_BOARDS")}
|
onClick={() => openModal("IMPORT_BOARDS")}
|
||||||
iconLeft={
|
|
||||||
<HiArrowDownTray aria-hidden="true" className="h-4 w-4" />
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
|
<div className="flex h-5 w-5 items-center">
|
||||||
|
<HiArrowDownTray
|
||||||
|
className="-mr-0.5 h-4 w-4"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{t`Import`}
|
{t`Import`}
|
||||||
</Button>
|
</button>
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="primary"
|
className="flex items-center gap-x-1.5 rounded-md bg-light-1000 px-3 py-2 text-sm font-semibold text-light-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 dark:bg-dark-1000 dark:text-dark-50"
|
||||||
onClick={() => openModal("NEW_BOARD")}
|
onClick={() => openModal("NEW_BOARD")}
|
||||||
iconLeft={
|
|
||||||
<HiOutlinePlusSmall aria-hidden="true" className="h-4 w-4" />
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
|
<div className="h-5 w-5 items-center">
|
||||||
|
<HiOutlinePlusSmall
|
||||||
|
className="-mr-0.5 h-5 w-5"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{t`New`}
|
{t`New`}
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal modalSize={modalContentType === "NEW_FEEDBACK" ? "md" : "sm"}>
|
<Modal>
|
||||||
{modalContentType === "NEW_FEEDBACK" && <FeedbackModal />}
|
|
||||||
{modalContentType === "NEW_BOARD" && <NewBoardForm />}
|
{modalContentType === "NEW_BOARD" && <NewBoardForm />}
|
||||||
{modalContentType === "IMPORT_BOARDS" && <ImportBoardsForm />}
|
{modalContentType === "IMPORT_BOARDS" && <ImportBoardsForm />}
|
||||||
{modalContentType === "NEW_WORKSPACE" && <NewWorkspaceForm />}
|
{modalContentType === "NEW_WORKSPACE" && <NewWorkspaceForm />}
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ const Comment = ({
|
|||||||
html={watch("comment")}
|
html={watch("comment")}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onChange={(e) => setValue("comment", e.target.value)}
|
onChange={(e) => setValue("comment", e.target.value)}
|
||||||
className="block w-full max-w-[800px] border-0 bg-transparent py-1.5 text-sm text-light-900 focus-visible:outline-none dark:text-dark-1000 sm:text-sm sm:leading-6"
|
className="block w-full max-w-[800px] border-0 bg-transparent py-1.5 text-light-900 focus-visible:outline-none dark:text-dark-1000 sm:text-sm sm:leading-6"
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end space-x-2">
|
<div className="flex justify-end space-x-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export default function LabelSelector({
|
|||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 pl-2 text-left text-sm text-neutral-900 hover:border-light-300 hover:bg-light-200 dark:border-dark-50 dark:text-dark-1000 dark:hover:border-dark-200 dark:hover:bg-dark-100">
|
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-200 pl-2 text-left text-sm text-neutral-900 hover:bg-light-300 dark:border-dark-100 dark:text-dark-1000 dark:hover:border-dark-300 dark:hover:bg-dark-200">
|
||||||
<HiMiniPlus size={22} className="pr-2" />
|
<HiMiniPlus size={22} className="pr-2" />
|
||||||
{t`Add label`}
|
{t`Add label`}
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default function ListSelector({
|
|||||||
}}
|
}}
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 py-1 pl-2 text-left text-sm text-neutral-900 hover:border-light-300 hover:bg-light-200 dark:border-dark-50 dark:text-dark-1000 dark:hover:border-dark-200 dark:hover:bg-dark-100">
|
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-200 py-1 pl-2 text-left text-sm text-neutral-900 hover:border-light-300 hover:bg-light-300 dark:border-dark-100 dark:text-dark-1000 dark:hover:border-dark-300 dark:hover:bg-dark-200">
|
||||||
{selectedList?.value}
|
{selectedList?.value}
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
</CheckboxDropdown>
|
</CheckboxDropdown>
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export default function MemberSelector({
|
|||||||
createNewItemLabel={t`Invite member`}
|
createNewItemLabel={t`Invite member`}
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 py-1 pl-2 text-left text-sm text-neutral-900 hover:border-light-300 hover:bg-light-200 dark:border-dark-50 dark:text-dark-1000 dark:hover:border-dark-200 dark:hover:bg-dark-100">
|
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-200 py-1 pl-2 text-left text-sm text-neutral-900 hover:bg-light-300 dark:border-dark-100 dark:text-dark-1000 dark:hover:border-dark-300 dark:hover:bg-dark-200">
|
||||||
{selectedMembers.length ? (
|
{selectedMembers.length ? (
|
||||||
<div className="isolate flex justify-end -space-x-1 overflow-hidden">
|
<div className="isolate flex justify-end -space-x-1 overflow-hidden">
|
||||||
{selectedMembers.map(({ value, imageUrl }) => (
|
{selectedMembers.map(({ value, imageUrl }) => (
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => {
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="flex w-full max-w-[800px] flex-col rounded-xl border border-light-600 bg-light-100 p-4 text-light-900 focus-visible:outline-none dark:border-dark-400 dark:bg-dark-100 dark:text-dark-1000 sm:text-sm sm:leading-6"
|
className="flex w-full max-w-[1000px] flex-col rounded-xl border border-light-600 bg-light-200 p-4 text-light-900 focus-visible:outline-none dark:border-dark-400 dark:bg-dark-100 dark:text-dark-1000 sm:text-sm sm:leading-6"
|
||||||
>
|
>
|
||||||
<ContentEditable
|
<ContentEditable
|
||||||
placeholder={t`Add a comment...`}
|
placeholder={t`Add a comment...`}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { IoChevronForwardSharp } from "react-icons/io5";
|
|||||||
|
|
||||||
import Avatar from "~/components/Avatar";
|
import Avatar from "~/components/Avatar";
|
||||||
import Editor from "~/components/Editor";
|
import Editor from "~/components/Editor";
|
||||||
import FeedbackModal from "~/components/FeedbackModal";
|
|
||||||
import { LabelForm } from "~/components/LabelForm";
|
import { LabelForm } from "~/components/LabelForm";
|
||||||
import LabelIcon from "~/components/LabelIcon";
|
import LabelIcon from "~/components/LabelIcon";
|
||||||
import Modal from "~/components/modal";
|
import Modal from "~/components/modal";
|
||||||
@@ -33,18 +32,29 @@ interface FormValues {
|
|||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CardRightPanel() {
|
export default function CardPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { modalContentType, entityId } = useModal();
|
||||||
|
const { showPopup } = usePopup();
|
||||||
|
const { workspace } = useWorkspace();
|
||||||
|
|
||||||
const cardId = Array.isArray(router.query.cardId)
|
const cardId = Array.isArray(router.query.cardId)
|
||||||
? router.query.cardId[0]
|
? router.query.cardId[0]
|
||||||
: router.query.cardId;
|
: router.query.cardId;
|
||||||
|
|
||||||
const { data: card } = api.card.byId.useQuery({
|
const { data: card, isLoading } = api.card.byId.useQuery({
|
||||||
cardPublicId: cardId ?? "",
|
cardPublicId: cardId ?? "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const refetchCard = async () => {
|
||||||
|
if (cardId) await utils.card.byId.refetch({ cardPublicId: cardId });
|
||||||
|
};
|
||||||
|
|
||||||
const board = card?.list.board;
|
const board = card?.list.board;
|
||||||
|
const boardId = board?.publicId;
|
||||||
const labels = board?.labels;
|
const labels = board?.labels;
|
||||||
|
const activities = card?.activities;
|
||||||
const workspaceMembers = board?.workspace.members;
|
const workspaceMembers = board?.workspace.members;
|
||||||
const selectedLabels = card?.labels;
|
const selectedLabels = card?.labels;
|
||||||
const selectedMembers = card?.members;
|
const selectedMembers = card?.members;
|
||||||
@@ -99,59 +109,6 @@ export function CardRightPanel() {
|
|||||||
};
|
};
|
||||||
}) ?? [];
|
}) ?? [];
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full w-[360px] border-l-[1px] border-light-300 bg-light-50 p-8 text-light-900 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900">
|
|
||||||
<div className="mb-4 flex w-full flex-row">
|
|
||||||
<p className="my-2 mb-2 w-[100px] text-sm font-medium">{t`List`}</p>
|
|
||||||
<ListSelector
|
|
||||||
cardPublicId={cardId ?? ""}
|
|
||||||
lists={formattedLists}
|
|
||||||
isLoading={!card}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4 flex w-full flex-row">
|
|
||||||
<p className="my-2 mb-2 w-[100px] text-sm font-medium">{t`Labels`}</p>
|
|
||||||
<LabelSelector
|
|
||||||
cardPublicId={cardId ?? ""}
|
|
||||||
labels={formattedLabels}
|
|
||||||
isLoading={!card}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-row">
|
|
||||||
<p className="my-2 mb-2 w-[100px] text-sm font-medium">{t`Members`}</p>
|
|
||||||
<MemberSelector
|
|
||||||
cardPublicId={cardId ?? ""}
|
|
||||||
members={formattedMembers}
|
|
||||||
isLoading={!card}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CardPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const { modalContentType, entityId } = useModal();
|
|
||||||
const { showPopup } = usePopup();
|
|
||||||
const { workspace } = useWorkspace();
|
|
||||||
|
|
||||||
const cardId = Array.isArray(router.query.cardId)
|
|
||||||
? router.query.cardId[0]
|
|
||||||
: router.query.cardId;
|
|
||||||
|
|
||||||
const { data: card, isLoading } = api.card.byId.useQuery({
|
|
||||||
cardPublicId: cardId ?? "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const refetchCard = async () => {
|
|
||||||
if (cardId) await utils.card.byId.refetch({ cardPublicId: cardId });
|
|
||||||
};
|
|
||||||
|
|
||||||
const board = card?.list.board;
|
|
||||||
const boardId = board?.publicId;
|
|
||||||
const activities = card?.activities;
|
|
||||||
|
|
||||||
const updateCard = api.card.update.useMutation({
|
const updateCard = api.card.update.useMutation({
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showPopup({
|
showPopup({
|
||||||
@@ -181,6 +138,8 @@ export default function CardPage() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const description = watch("description");
|
||||||
|
|
||||||
if (!cardId) return <></>;
|
if (!cardId) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -189,9 +148,9 @@ export default function CardPage() {
|
|||||||
title={t`${card?.title ?? "Card"} | ${board?.name ?? "Board"}`}
|
title={t`${card?.title ?? "Card"} | ${board?.name ?? "Board"}`}
|
||||||
/>
|
/>
|
||||||
<div className="flex h-full flex-1 flex-row">
|
<div className="flex h-full flex-1 flex-row">
|
||||||
<div className="m-auto flex h-full w-full max-w-[800px] flex-col overflow-hidden">
|
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||||
<div className="h-full max-h-[calc(100dvh-3rem)] overflow-y-auto p-6 md:max-h-[calc(100dvh-4rem)] md:p-8">
|
<div className="h-full max-h-[calc(100vh-4rem)] overflow-y-auto p-8">
|
||||||
<div className="mb-8 flex w-full items-center justify-between md:mt-6">
|
<div className="mb-8 flex w-full items-center justify-between">
|
||||||
{!card && isLoading && (
|
{!card && isLoading && (
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<div className="h-[2.3rem] w-[150px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />
|
<div className="h-[2.3rem] w-[150px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />
|
||||||
@@ -237,7 +196,7 @@ export default function CardPage() {
|
|||||||
</div>
|
</div>
|
||||||
{card && (
|
{card && (
|
||||||
<>
|
<>
|
||||||
<div className="mb-10 flex w-full max-w-2xl flex-col justify-between">
|
<div className="mb-10 flex w-full max-w-2xl justify-between">
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="w-full space-y-6"
|
className="w-full space-y-6"
|
||||||
@@ -251,7 +210,7 @@ export default function CardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t-[1px] border-light-300 pt-12 dark:border-dark-300">
|
<div className="border-t-[1px] border-light-600 pt-12 dark:border-dark-400">
|
||||||
<h2 className="text-md pb-4 font-medium text-light-900 dark:text-dark-1000">
|
<h2 className="text-md pb-4 font-medium text-light-900 dark:text-dark-1000">
|
||||||
{t`Activity`}
|
{t`Activity`}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -271,9 +230,34 @@ export default function CardPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="w-[475px] border-l-[1px] border-light-600 bg-light-200 p-8 text-light-900 dark:border-dark-400 dark:bg-dark-100 dark:text-dark-900">
|
||||||
|
<div className="mb-4 flex w-full">
|
||||||
|
<p className="my-2 w-[100px] text-sm">{t`List`}</p>
|
||||||
|
<ListSelector
|
||||||
|
cardPublicId={cardId}
|
||||||
|
lists={formattedLists}
|
||||||
|
isLoading={!card}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 flex w-full">
|
||||||
|
<p className="my-2 w-[100px] text-sm">{t`Labels`}</p>
|
||||||
|
<LabelSelector
|
||||||
|
cardPublicId={cardId}
|
||||||
|
labels={formattedLabels}
|
||||||
|
isLoading={!card}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full">
|
||||||
|
<p className="my-2 w-[100px] text-sm">{t`Members`}</p>
|
||||||
|
<MemberSelector
|
||||||
|
cardPublicId={cardId}
|
||||||
|
members={formattedMembers}
|
||||||
|
isLoading={!card}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Modal modalSize={modalContentType === "NEW_FEEDBACK" ? "md" : "sm"}>
|
<Modal>
|
||||||
{modalContentType === "NEW_FEEDBACK" && <FeedbackModal />}
|
|
||||||
{modalContentType === "NEW_LABEL" && (
|
{modalContentType === "NEW_LABEL" && (
|
||||||
<LabelForm boardPublicId={boardId ?? ""} refetch={refetchCard} />
|
<LabelForm boardPublicId={boardId ?? ""} refetch={refetchCard} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default function HomeView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <div className="relative overflow-hidden rounded-full bg-gradient-to-b from-light-300 to-light-400 p-[2px] dark:from-dark-300 dark:to-dark-400">
|
<div className="relative overflow-hidden rounded-full bg-gradient-to-b from-light-300 to-light-400 p-[2px] dark:from-dark-300 dark:to-dark-400">
|
||||||
<div className="relative z-10 rounded-full bg-light-50 dark:bg-dark-50">
|
<div className="relative z-10 rounded-full bg-light-50 dark:bg-dark-50">
|
||||||
<Link
|
<Link
|
||||||
href="https://news.ycombinator.com/item?id=44157177"
|
href="https://news.ycombinator.com/item?id=44157177"
|
||||||
@@ -58,7 +58,7 @@ export default function HomeView() {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-2 text-center text-4xl font-bold text-light-1000 dark:text-dark-1000 lg:text-5xl">
|
<p className="mt-2 text-center text-4xl font-bold text-light-1000 dark:text-dark-1000 lg:text-5xl">
|
||||||
|
|||||||
@@ -2,12 +2,9 @@ import { t } from "@lingui/core/macro";
|
|||||||
import { HiEllipsisHorizontal, HiOutlinePlusSmall } from "react-icons/hi2";
|
import { HiEllipsisHorizontal, HiOutlinePlusSmall } from "react-icons/hi2";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { authClient } from "@kan/auth/client";
|
|
||||||
|
|
||||||
import Avatar from "~/components/Avatar";
|
import Avatar from "~/components/Avatar";
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import Dropdown from "~/components/Dropdown";
|
import Dropdown from "~/components/Dropdown";
|
||||||
import FeedbackModal from "~/components/FeedbackModal";
|
|
||||||
import Modal from "~/components/modal";
|
import Modal from "~/components/modal";
|
||||||
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||||
import { PageHead } from "~/components/PageHead";
|
import { PageHead } from "~/components/PageHead";
|
||||||
@@ -29,7 +26,6 @@ export default function MembersPage() {
|
|||||||
|
|
||||||
const TableRow = ({
|
const TableRow = ({
|
||||||
memberPublicId,
|
memberPublicId,
|
||||||
memberId,
|
|
||||||
memberName,
|
memberName,
|
||||||
memberEmail,
|
memberEmail,
|
||||||
memberImage,
|
memberImage,
|
||||||
@@ -39,7 +35,6 @@ export default function MembersPage() {
|
|||||||
showSkeleton,
|
showSkeleton,
|
||||||
}: {
|
}: {
|
||||||
memberPublicId?: string;
|
memberPublicId?: string;
|
||||||
memberId?: string | null | undefined;
|
|
||||||
memberName?: string | null | undefined;
|
memberName?: string | null | undefined;
|
||||||
memberEmail?: string | null | undefined;
|
memberEmail?: string | null | undefined;
|
||||||
memberImage?: string | null | undefined;
|
memberImage?: string | null | undefined;
|
||||||
@@ -48,19 +43,13 @@ export default function MembersPage() {
|
|||||||
isLastRow?: boolean;
|
isLastRow?: boolean;
|
||||||
showSkeleton?: boolean;
|
showSkeleton?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { data: session } = authClient.useSession();
|
|
||||||
return (
|
return (
|
||||||
<tr className="rounded-b-lg">
|
<tr className="rounded-b-lg">
|
||||||
<td
|
<td className={twMerge("w-[65%]", isLastRow ? "rounded-bl-lg" : "")}>
|
||||||
className={twMerge(
|
<div className="flex items-center p-4">
|
||||||
"w-full sm:w-[65%]",
|
|
||||||
isLastRow ? "rounded-bl-lg" : "",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center p-2 sm:p-4">
|
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
{showSkeleton ? (
|
{showSkeleton ? (
|
||||||
<div className="h-8 w-8 animate-pulse rounded-full bg-light-200 dark:bg-dark-200 sm:h-9 sm:w-9" />
|
<div className="h-9 w-9 animate-pulse rounded-full bg-light-200 dark:bg-dark-200" />
|
||||||
) : (
|
) : (
|
||||||
<Avatar
|
<Avatar
|
||||||
name={memberName ?? ""}
|
name={memberName ?? ""}
|
||||||
@@ -74,7 +63,7 @@ export default function MembersPage() {
|
|||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<p
|
<p
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"mr-2 truncate text-xs font-medium text-neutral-900 dark:text-dark-1000 sm:text-sm",
|
"mr-2 text-sm font-medium text-neutral-900 dark:text-dark-1000",
|
||||||
showSkeleton &&
|
showSkeleton &&
|
||||||
"md mb-2 h-3 w-[125px] animate-pulse rounded-sm bg-light-200 dark:bg-dark-200",
|
"md mb-2 h-3 w-[125px] animate-pulse rounded-sm bg-light-200 dark:bg-dark-200",
|
||||||
)}
|
)}
|
||||||
@@ -84,7 +73,7 @@ export default function MembersPage() {
|
|||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"truncate text-xs text-dark-900 sm:text-sm",
|
"truncate text-sm text-dark-900",
|
||||||
showSkeleton &&
|
showSkeleton &&
|
||||||
"h-3 w-[175px] animate-pulse rounded-sm bg-light-200 dark:bg-dark-200",
|
"h-3 w-[175px] animate-pulse rounded-sm bg-light-200 dark:bg-dark-200",
|
||||||
)}
|
)}
|
||||||
@@ -97,15 +86,15 @@ export default function MembersPage() {
|
|||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"w-auto min-w-[120px] sm:w-[35%] sm:min-w-[150px]",
|
"w-[35%] min-w-[150px]",
|
||||||
isLastRow && "rounded-br-lg",
|
isLastRow && "rounded-br-lg",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between px-2 sm:px-3">
|
<div className="flex w-full items-center justify-between px-3">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center">
|
<div>
|
||||||
<span
|
<span
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"inline-flex items-center rounded-md bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-medium text-emerald-400 ring-1 ring-inset ring-emerald-500/20 sm:text-[11px]",
|
"inline-flex items-center rounded-md bg-emerald-500/10 px-1.5 py-0.5 text-[11px] font-medium text-emerald-400 ring-1 ring-inset ring-emerald-500/20",
|
||||||
showSkeleton &&
|
showSkeleton &&
|
||||||
"h-5 w-[50px] animate-pulse bg-light-200 ring-0 dark:bg-dark-200",
|
"h-5 w-[50px] animate-pulse bg-light-200 ring-0 dark:bg-dark-200",
|
||||||
)}
|
)}
|
||||||
@@ -114,7 +103,7 @@ export default function MembersPage() {
|
|||||||
memberRole.charAt(0).toUpperCase() + memberRole.slice(1)}
|
memberRole.charAt(0).toUpperCase() + memberRole.slice(1)}
|
||||||
</span>
|
</span>
|
||||||
{memberStatus === "invited" && (
|
{memberStatus === "invited" && (
|
||||||
<span className="mt-1 inline-flex items-center rounded-md bg-gray-500/10 px-1.5 py-0.5 text-[10px] font-medium text-gray-400 ring-1 ring-inset ring-gray-500/20 sm:ml-2 sm:mt-0 sm:text-[11px]">
|
<span className="ml-2 inline-flex items-center rounded-md bg-gray-500/10 px-1.5 py-0.5 text-[11px] font-medium text-gray-400 ring-1 ring-inset ring-gray-500/20">
|
||||||
{t`Pending`}
|
{t`Pending`}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -125,26 +114,24 @@ export default function MembersPage() {
|
|||||||
(workspace.role !== "admin" || showSkeleton) && "hidden",
|
(workspace.role !== "admin" || showSkeleton) && "hidden",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{session?.user.id !== memberId && (
|
<Dropdown
|
||||||
<Dropdown
|
items={[
|
||||||
items={[
|
{
|
||||||
{
|
label: t`Remove member`,
|
||||||
label: t`Remove member`,
|
action: () =>
|
||||||
action: () =>
|
openModal(
|
||||||
openModal(
|
"REMOVE_MEMBER",
|
||||||
"REMOVE_MEMBER",
|
memberPublicId,
|
||||||
memberPublicId,
|
memberEmail ?? "",
|
||||||
memberEmail ?? "",
|
),
|
||||||
),
|
},
|
||||||
},
|
]}
|
||||||
]}
|
>
|
||||||
>
|
<HiEllipsisHorizontal
|
||||||
<HiEllipsisHorizontal
|
size={25}
|
||||||
size={20}
|
className="text-light-900 dark:text-dark-900"
|
||||||
className="text-light-900 dark:text-dark-900 sm:size-[25px]"
|
/>
|
||||||
/>
|
</Dropdown>
|
||||||
</Dropdown>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -155,7 +142,7 @@ export default function MembersPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title={t`Members | ${workspace.name ?? "Workspace"}`} />
|
<PageHead title={t`Members | ${workspace.name ?? "Workspace"}`} />
|
||||||
<div className="m-auto h-full max-w-[1100px] p-6 px-5 md:px-28 md:py-12">
|
<div className="m-auto max-w-[1600px] px-28 py-12">
|
||||||
<div className="mb-8 flex w-full justify-between">
|
<div className="mb-8 flex w-full justify-between">
|
||||||
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
|
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
|
||||||
{t`Members`}
|
{t`Members`}
|
||||||
@@ -172,21 +159,21 @@ export default function MembersPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 flow-root">
|
<div className="mt-8 flow-root">
|
||||||
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
<div className="-mx-4 -my-2 overflow-x-visible sm:-mx-6 lg:-mx-8">
|
||||||
<div className="inline-block min-w-full px-4 py-2 align-middle sm:px-6 lg:px-8">
|
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||||
<div className="h-full shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
|
<div className="h-full shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
|
||||||
<table className="min-w-full divide-y divide-light-600 dark:divide-dark-600">
|
<table className="min-w-full divide-y divide-light-600 dark:divide-dark-600">
|
||||||
<thead className="rounded-t-lg bg-light-300 dark:bg-dark-200">
|
<thead className="rounded-t-lg bg-light-300 dark:bg-dark-200">
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="w-full rounded-tl-lg py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-light-900 dark:text-dark-900 sm:w-[65%] sm:pl-6"
|
className="w-[65%] rounded-tl-lg py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-light-900 dark:text-dark-900 sm:pl-6"
|
||||||
>
|
>
|
||||||
{t`User`}
|
{t`User`}
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="w-auto whitespace-nowrap rounded-tr-lg px-3 py-3.5 text-left text-sm font-semibold text-light-900 dark:text-dark-900 sm:w-[35%]"
|
className="w-[35%] rounded-tr-lg px-3 py-3.5 text-left text-sm font-semibold text-light-900 dark:text-dark-900"
|
||||||
>
|
>
|
||||||
{t`Role`}
|
{t`Role`}
|
||||||
</th>
|
</th>
|
||||||
@@ -198,7 +185,6 @@ export default function MembersPage() {
|
|||||||
<TableRow
|
<TableRow
|
||||||
key={member.publicId}
|
key={member.publicId}
|
||||||
memberPublicId={member.publicId}
|
memberPublicId={member.publicId}
|
||||||
memberId={member.user?.id}
|
|
||||||
memberName={member.user?.name}
|
memberName={member.user?.name}
|
||||||
memberEmail={member.user?.email ?? member.email}
|
memberEmail={member.user?.email ?? member.email}
|
||||||
memberImage={member.user?.image}
|
memberImage={member.user?.image}
|
||||||
@@ -222,8 +208,7 @@ export default function MembersPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal modalSize={modalContentType === "NEW_FEEDBACK" ? "md" : "sm"}>
|
<Modal>
|
||||||
{modalContentType === "NEW_FEEDBACK" && <FeedbackModal />}
|
|
||||||
{modalContentType === "NEW_WORKSPACE" && <NewWorkspaceForm />}
|
{modalContentType === "NEW_WORKSPACE" && <NewWorkspaceForm />}
|
||||||
{modalContentType === "INVITE_MEMBER" && <InviteMemberForm />}
|
{modalContentType === "INVITE_MEMBER" && <InviteMemberForm />}
|
||||||
{modalContentType === "REMOVE_MEMBER" && <DeleteMemberConfirmation />}
|
{modalContentType === "REMOVE_MEMBER" && <DeleteMemberConfirmation />}
|
||||||
|
|||||||
@@ -105,15 +105,13 @@ export default function PublicBoardView() {
|
|||||||
{data?.name}
|
{data?.name}
|
||||||
</h1>
|
</h1>
|
||||||
)}
|
)}
|
||||||
{data && (
|
<div className="flex items-center space-x-2">
|
||||||
<div className="flex items-center space-x-2">
|
<Filters
|
||||||
<Filters
|
labels={data?.labels ?? []}
|
||||||
labels={data.labels ?? []}
|
members={[]}
|
||||||
members={[]}
|
isLoading={isLoading}
|
||||||
isLoading={isLoading}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="scrollbar-w-none scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-h-[8px] relative flex-1 overflow-y-hidden overflow-x-scroll overscroll-contain scrollbar scrollbar-track-light-200 scrollbar-thumb-light-400 dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-300">
|
<div className="scrollbar-w-none scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-h-[8px] relative flex-1 overflow-y-hidden overflow-x-scroll overscroll-contain scrollbar scrollbar-track-light-200 scrollbar-thumb-light-400 dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-300">
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { t } from "@lingui/core/macro";
|
|||||||
import { env } from "next-runtime-env";
|
import { env } from "next-runtime-env";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { generateUID } from "@kan/shared/utils";
|
|
||||||
|
|
||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { getAvatarUrl } from "~/utils/helpers";
|
import { getAvatarUrl } from "~/utils/helpers";
|
||||||
@@ -60,7 +58,7 @@ export default function Avatar({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fileExt = file.name.split(".").pop();
|
const fileExt = file.name.split(".").pop();
|
||||||
const fileName = `${userId}/avatar-${generateUID()}.${fileExt}`;
|
const fileName = `${userId}/avatar.${fileExt}`;
|
||||||
|
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { env } from "next-runtime-env";
|
import { env } from "next-runtime-env";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect } from "react";
|
||||||
import { HiMiniArrowTopRightOnSquare } from "react-icons/hi2";
|
import { HiMiniArrowTopRightOnSquare } from "react-icons/hi2";
|
||||||
|
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import FeedbackModal from "~/components/FeedbackModal";
|
|
||||||
import { LanguageSelector } from "~/components/LanguageSelector";
|
import { LanguageSelector } from "~/components/LanguageSelector";
|
||||||
import Modal from "~/components/modal";
|
import Modal from "~/components/modal";
|
||||||
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||||
@@ -29,9 +27,6 @@ export default function SettingsPage() {
|
|||||||
const { workspace } = useWorkspace();
|
const { workspace } = useWorkspace();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
const router = useRouter();
|
|
||||||
const workspaceUrlSectionRef = useRef<HTMLDivElement>(null);
|
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const { data } = api.user.getUser.useQuery();
|
const { data } = api.user.getUser.useQuery();
|
||||||
|
|
||||||
@@ -64,24 +59,6 @@ export default function SettingsPage() {
|
|||||||
};
|
};
|
||||||
}, [refetchIntegrations]);
|
}, [refetchIntegrations]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
router.query.edit === "workspace_url" &&
|
|
||||||
workspaceUrlSectionRef.current &&
|
|
||||||
scrollContainerRef.current
|
|
||||||
) {
|
|
||||||
const element = workspaceUrlSectionRef.current;
|
|
||||||
const container = scrollContainerRef.current;
|
|
||||||
|
|
||||||
container.scrollTop = element.offsetTop - 40;
|
|
||||||
|
|
||||||
const input = element.querySelector('input[type="text"]');
|
|
||||||
if (input instanceof HTMLInputElement) {
|
|
||||||
input.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [router.query.edit]);
|
|
||||||
|
|
||||||
const { mutateAsync: disconnectTrello } =
|
const { mutateAsync: disconnectTrello } =
|
||||||
api.integration.disconnect.useMutation({
|
api.integration.disconnect.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -127,12 +104,9 @@ export default function SettingsPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||||
<div
|
<div className="h-full max-h-[calc(100vh-4rem)] overflow-y-auto">
|
||||||
ref={scrollContainerRef}
|
|
||||||
className="h-full max-h-[calc(100vdh-3rem)] overflow-y-auto md:max-h-[calc(100vdh-4rem)]"
|
|
||||||
>
|
|
||||||
<PageHead title={t`Settings | ${workspace.name ?? "Workspace"}`} />
|
<PageHead title={t`Settings | ${workspace.name ?? "Workspace"}`} />
|
||||||
<div className="m-auto max-w-[1100px] px-5 py-6 md:px-28 md:py-12">
|
<div className="px-28 py-12">
|
||||||
<div className="mb-8 flex w-full justify-between">
|
<div className="mb-8 flex w-full justify-between">
|
||||||
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
|
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
|
||||||
{t`Settings`}
|
{t`Settings`}
|
||||||
@@ -160,16 +134,14 @@ export default function SettingsPage() {
|
|||||||
workspaceName={workspace.name}
|
workspaceName={workspace.name}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div ref={workspaceUrlSectionRef}>
|
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
|
||||||
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
|
{t`Workspace URL`}
|
||||||
{t`Workspace URL`}
|
</h2>
|
||||||
</h2>
|
<UpdateWorkspaceUrlForm
|
||||||
<UpdateWorkspaceUrlForm
|
workspacePublicId={workspace.publicId}
|
||||||
workspacePublicId={workspace.publicId}
|
workspaceUrl={workspace.slug ?? ""}
|
||||||
workspaceUrl={workspace.slug ?? ""}
|
workspacePlan={workspace.plan ?? "free"}
|
||||||
workspacePlan={workspace.plan ?? "free"}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
|
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
|
||||||
{t`Workspace description`}
|
{t`Workspace description`}
|
||||||
@@ -301,8 +273,7 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal modalSize={modalContentType === "NEW_FEEDBACK" ? "md" : "sm"}>
|
<Modal>
|
||||||
{modalContentType === "NEW_FEEDBACK" && <FeedbackModal />}
|
|
||||||
{modalContentType === "NEW_WORKSPACE" && <NewWorkspaceForm />}
|
{modalContentType === "NEW_WORKSPACE" && <NewWorkspaceForm />}
|
||||||
{modalContentType === "DELETE_WORKSPACE" && (
|
{modalContentType === "DELETE_WORKSPACE" && (
|
||||||
<DeleteWorkspaceConfirmation />
|
<DeleteWorkspaceConfirmation />
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ services:
|
|||||||
- S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}
|
- S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}
|
||||||
- S3_REGION=${S3_REGION}
|
- S3_REGION=${S3_REGION}
|
||||||
- S3_ENDPOINT=${S3_ENDPOINT}
|
- S3_ENDPOINT=${S3_ENDPOINT}
|
||||||
- S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE}
|
|
||||||
- NEXT_PUBLIC_STORAGE_URL=${NEXT_PUBLIC_STORAGE_URL}
|
- NEXT_PUBLIC_STORAGE_URL=${NEXT_PUBLIC_STORAGE_URL}
|
||||||
- NEXT_PUBLIC_AVATAR_BUCKET_NAME=${NEXT_PUBLIC_AVATAR_BUCKET_NAME}
|
- NEXT_PUBLIC_AVATAR_BUCKET_NAME=${NEXT_PUBLIC_AVATAR_BUCKET_NAME}
|
||||||
- NEXT_PUBLIC_STORAGE_DOMAIN=${NEXT_PUBLIC_STORAGE_DOMAIN}
|
- NEXT_PUBLIC_STORAGE_DOMAIN=${NEXT_PUBLIC_STORAGE_DOMAIN}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ services:
|
|||||||
- S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}
|
- S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}
|
||||||
- S3_REGION=${S3_REGION}
|
- S3_REGION=${S3_REGION}
|
||||||
- S3_ENDPOINT=${S3_ENDPOINT}
|
- S3_ENDPOINT=${S3_ENDPOINT}
|
||||||
- S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE}
|
|
||||||
- NEXT_PUBLIC_STORAGE_URL=${NEXT_PUBLIC_STORAGE_URL}
|
- NEXT_PUBLIC_STORAGE_URL=${NEXT_PUBLIC_STORAGE_URL}
|
||||||
- NEXT_PUBLIC_AVATAR_BUCKET_NAME=${NEXT_PUBLIC_AVATAR_BUCKET_NAME}
|
- NEXT_PUBLIC_AVATAR_BUCKET_NAME=${NEXT_PUBLIC_AVATAR_BUCKET_NAME}
|
||||||
- NEXT_PUBLIC_STORAGE_DOMAIN=${NEXT_PUBLIC_STORAGE_DOMAIN}
|
- NEXT_PUBLIC_STORAGE_DOMAIN=${NEXT_PUBLIC_STORAGE_DOMAIN}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
"@kan/stripe": "workspace:^",
|
"@kan/stripe": "workspace:^",
|
||||||
"@trpc/server": "catalog:",
|
"@trpc/server": "catalog:",
|
||||||
"superjson": "2.2.1",
|
"superjson": "2.2.1",
|
||||||
"trpc-to-openapi": "^2.3.2",
|
"trpc-to-openapi": "^2.1.0",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import { z } from "zod";
|
|||||||
import * as boardRepo from "@kan/db/repository/board.repo";
|
import * as boardRepo from "@kan/db/repository/board.repo";
|
||||||
import * as cardRepo from "@kan/db/repository/card.repo";
|
import * as cardRepo from "@kan/db/repository/card.repo";
|
||||||
import * as activityRepo from "@kan/db/repository/cardActivity.repo";
|
import * as activityRepo from "@kan/db/repository/cardActivity.repo";
|
||||||
import * as labelRepo from "@kan/db/repository/label.repo";
|
|
||||||
import * as listRepo from "@kan/db/repository/list.repo";
|
import * as listRepo from "@kan/db/repository/list.repo";
|
||||||
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||||
import { colours } from "@kan/shared/constants";
|
|
||||||
import { generateSlug, generateUID } from "@kan/shared/utils";
|
import { generateSlug, generateUID } from "@kan/shared/utils";
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||||
@@ -151,10 +149,8 @@ export const boardRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string().min(1).max(100),
|
name: z.string().min(1),
|
||||||
workspacePublicId: z.string().min(12),
|
workspacePublicId: z.string().min(12),
|
||||||
lists: z.array(z.string().min(1)),
|
|
||||||
labels: z.array(z.string().min(1)),
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.custom<Awaited<ReturnType<typeof boardRepo.create>>>())
|
.output(z.custom<Awaited<ReturnType<typeof boardRepo.create>>>())
|
||||||
@@ -203,30 +199,6 @@ export const boardRouter = createTRPCRouter({
|
|||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (input.lists.length) {
|
|
||||||
const listInputs = input.lists.map((list, index) => ({
|
|
||||||
publicId: generateUID(),
|
|
||||||
name: list,
|
|
||||||
boardId: result.id,
|
|
||||||
createdBy: userId,
|
|
||||||
index,
|
|
||||||
}));
|
|
||||||
|
|
||||||
await listRepo.bulkCreate(ctx.db, listInputs);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.labels.length) {
|
|
||||||
const labelInputs = input.labels.map((label, index) => ({
|
|
||||||
publicId: generateUID(),
|
|
||||||
name: label,
|
|
||||||
boardId: result.id,
|
|
||||||
createdBy: userId,
|
|
||||||
colourCode: colours[index % colours.length]?.code ?? "#0d9488",
|
|
||||||
}));
|
|
||||||
|
|
||||||
await labelRepo.bulkCreate(ctx.db, labelInputs);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
@@ -276,21 +248,6 @@ export const boardRouter = createTRPCRouter({
|
|||||||
|
|
||||||
await assertUserInWorkspace(ctx.db, userId, board.workspaceId);
|
await assertUserInWorkspace(ctx.db, userId, board.workspaceId);
|
||||||
|
|
||||||
if (input.slug) {
|
|
||||||
const isBoardSlugAvailable = await boardRepo.isBoardSlugAvailable(
|
|
||||||
ctx.db,
|
|
||||||
input.slug,
|
|
||||||
board.workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isBoardSlugAvailable) {
|
|
||||||
throw new TRPCError({
|
|
||||||
message: `Board slug ${input.slug} is not available`,
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await boardRepo.update(ctx.db, {
|
const result = await boardRepo.update(ctx.db, {
|
||||||
name: input.name,
|
name: input.name,
|
||||||
slug: input.slug,
|
slug: input.slug,
|
||||||
@@ -395,52 +352,40 @@ export const boardRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
checkSlugAvailability: publicProcedure
|
checkSlugAvailability: publicProcedure
|
||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
summary: "Check if a board slug is available",
|
summary: "Check if a board slug is available",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/boards/{boardPublicId}/check-slug-availability",
|
path: "/boards/{boardPublicId}/check-slug-availability",
|
||||||
description: "Checks if a board slug is available",
|
description: "Checks if a board slug is available",
|
||||||
tags: ["Boards"],
|
tags: ["Boards"],
|
||||||
protect: true,
|
protect: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
boardSlug: z
|
boardSlug: z
|
||||||
.string()
|
.string()
|
||||||
.min(3)
|
.min(3)
|
||||||
.max(24)
|
.max(24)
|
||||||
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
|
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
|
||||||
boardPublicId: z.string().min(12),
|
boardPublicId: z.string().min(12),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
isReserved: z.boolean(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const isBoardSlugAvailable = await boardRepo.isBoardSlugAvailable(
|
||||||
|
ctx.db,
|
||||||
|
input.boardSlug,
|
||||||
|
input.boardPublicId,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
isReserved: !isBoardSlugAvailable,
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
)
|
|
||||||
.output(
|
|
||||||
z.object({
|
|
||||||
isReserved: z.boolean(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.query(async ({ ctx, input }) => {
|
|
||||||
const board = await boardRepo.getWorkspaceAndBoardIdByBoardPublicId(
|
|
||||||
ctx.db,
|
|
||||||
input.boardPublicId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!board)
|
|
||||||
throw new TRPCError({
|
|
||||||
message: `Board with public ID ${input.boardPublicId} not found`,
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
});
|
|
||||||
|
|
||||||
const isBoardSlugAvailable = await boardRepo.isBoardSlugAvailable(
|
|
||||||
ctx.db,
|
|
||||||
input.boardSlug,
|
|
||||||
board.workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isReserved: !isBoardSlugAvailable,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ export const importRouter = createTRPCRouter({
|
|||||||
protect: true,
|
protect: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(z.void())
|
|
||||||
.output(z.array(z.object({ id: z.string(), name: z.string() })))
|
.output(z.array(z.object({ id: z.string(), name: z.string() })))
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
const apiKey = apiKeys.trello;
|
const apiKey = apiKeys.trello;
|
||||||
|
|||||||
@@ -15,47 +15,22 @@ export const apiKeys = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const integrationRouter = createTRPCRouter({
|
export const integrationRouter = createTRPCRouter({
|
||||||
providers: protectedProcedure
|
providers: protectedProcedure.query(async ({ ctx }) => {
|
||||||
.meta({
|
const user = ctx.user;
|
||||||
openapi: {
|
|
||||||
summary: "Get integration providers",
|
|
||||||
method: "GET",
|
|
||||||
path: "/integration/providers",
|
|
||||||
description: "Retrieves all integration providers for the user",
|
|
||||||
tags: ["Integration"],
|
|
||||||
protect: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.input(z.void())
|
|
||||||
.output(
|
|
||||||
z.array(
|
|
||||||
z.object({
|
|
||||||
expiresAt: z.date(),
|
|
||||||
createdAt: z.date(),
|
|
||||||
updatedAt: z.date().nullable(),
|
|
||||||
userId: z.string(),
|
|
||||||
accessToken: z.string(),
|
|
||||||
refreshToken: z.string().nullable(),
|
|
||||||
provider: z.string(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.query(async ({ ctx }) => {
|
|
||||||
const user = ctx.user;
|
|
||||||
|
|
||||||
if (!user)
|
if (!user)
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
message: "User not authenticated",
|
message: "User not authenticated",
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
});
|
});
|
||||||
|
|
||||||
const integrations = await integrationsRepo.getProvidersForUser(
|
const integrations = await integrationsRepo.getProvidersForUser(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
user.id,
|
user.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return integrations;
|
return integrations;
|
||||||
}),
|
}),
|
||||||
disconnect: protectedProcedure
|
disconnect: protectedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
|
|||||||
@@ -8,12 +8,6 @@ import * as labelRepo from "@kan/db/repository/label.repo";
|
|||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
import { assertUserInWorkspace } from "../utils/auth";
|
import { assertUserInWorkspace } from "../utils/auth";
|
||||||
|
|
||||||
const labelSchema = z.object({
|
|
||||||
publicId: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
colourCode: z.string().nullable(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const labelRouter = createTRPCRouter({
|
export const labelRouter = createTRPCRouter({
|
||||||
byPublicId: protectedProcedure
|
byPublicId: protectedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@@ -27,7 +21,7 @@ export const labelRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(z.object({ labelPublicId: z.string().min(12) }))
|
.input(z.object({ labelPublicId: z.string().min(12) }))
|
||||||
.output(labelSchema)
|
.output(z.custom<Awaited<ReturnType<typeof labelRepo.getByPublicId>>>())
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -58,11 +52,7 @@ export const labelRouter = createTRPCRouter({
|
|||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return result;
|
||||||
publicId: result.publicId,
|
|
||||||
name: result.name,
|
|
||||||
colourCode: result.colourCode,
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@@ -82,7 +72,7 @@ export const labelRouter = createTRPCRouter({
|
|||||||
colourCode: z.string().length(7),
|
colourCode: z.string().length(7),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(labelSchema)
|
.output(z.custom<Awaited<ReturnType<typeof labelRepo.create>>>())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -118,11 +108,7 @@ export const labelRouter = createTRPCRouter({
|
|||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return result;
|
||||||
publicId: result.publicId,
|
|
||||||
name: result.name,
|
|
||||||
colourCode: result.colourCode,
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@@ -142,7 +128,7 @@ export const labelRouter = createTRPCRouter({
|
|||||||
colourCode: z.string().length(7),
|
colourCode: z.string().length(7),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(labelSchema)
|
.output(z.custom<Awaited<ReturnType<typeof labelRepo.update>>>())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -167,17 +153,7 @@ export const labelRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const result = await labelRepo.update(ctx.db, input);
|
const result = await labelRepo.update(ctx.db, input);
|
||||||
|
|
||||||
if (!result)
|
return result;
|
||||||
throw new TRPCError({
|
|
||||||
message: `Failed to update label`,
|
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
publicId: result.publicId,
|
|
||||||
name: result.name,
|
|
||||||
colourCode: result.colourCode,
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { authClient } from "@kan/auth/client";
|
||||||
import * as memberRepo from "@kan/db/repository/member.repo";
|
import * as memberRepo from "@kan/db/repository/member.repo";
|
||||||
import * as userRepo from "@kan/db/repository/user.repo";
|
import * as userRepo from "@kan/db/repository/user.repo";
|
||||||
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||||
@@ -77,22 +78,16 @@ export const memberRouter = createTRPCRouter({
|
|||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { status } = await ctx.auth.api.signInMagicLink({
|
const { error } = await authClient.signIn.magicLink({
|
||||||
email: input.email,
|
email: input.email,
|
||||||
callbackURL: `/boards?type=invite&memberPublicId=${invite.publicId}`,
|
callbackURL: `/boards?type=invite&memberPublicId=${invite.publicId}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!status) {
|
if (error)
|
||||||
console.error("Failed to send magic link invitation:", {
|
|
||||||
email: input.email,
|
|
||||||
callbackURL: `/boards?type=invite&memberPublicId=${invite.publicId}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
message: `Failed to send magic link invitation to user with email ${input.email}.`,
|
message: `Failed to send magic link to user with email ${input.email}`,
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return invite;
|
return invite;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -20,73 +20,58 @@ export interface User {
|
|||||||
stripeCustomerId?: string | null | undefined;
|
stripeCustomerId?: string | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createAuthWithHeaders = (
|
|
||||||
auth: ReturnType<typeof initAuth>,
|
|
||||||
headers: Headers,
|
|
||||||
) => {
|
|
||||||
return {
|
|
||||||
api: {
|
|
||||||
getSession: () => auth.api.getSession({ headers }),
|
|
||||||
signInMagicLink: (input: { email: string; callbackURL: string }) =>
|
|
||||||
auth.api.signInMagicLink({
|
|
||||||
headers,
|
|
||||||
body: { email: input.email, callbackURL: input.callbackURL },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
interface CreateContextOptions {
|
interface CreateContextOptions {
|
||||||
user: User | null | undefined;
|
user: User | null | undefined;
|
||||||
db: dbClient;
|
db: dbClient;
|
||||||
auth: ReturnType<typeof createAuthWithHeaders>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||||
return {
|
return {
|
||||||
user: opts.user,
|
user: opts.user,
|
||||||
db: opts.db,
|
db: opts.db,
|
||||||
auth: opts.auth,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createTRPCContext = async ({ req }: CreateNextContextOptions) => {
|
export const createTRPCContext = async ({ req }: CreateNextContextOptions) => {
|
||||||
const db = createDrizzleClient();
|
const db = createDrizzleClient();
|
||||||
const baseAuth = initAuth(db);
|
const auth = initAuth(db);
|
||||||
const headers = new Headers(req.headers as Record<string, string>);
|
|
||||||
const auth = createAuthWithHeaders(baseAuth, headers);
|
|
||||||
|
|
||||||
const session = await auth.api.getSession();
|
const session = await auth.api.getSession({
|
||||||
|
// @ts-expect-error
|
||||||
|
headers: new Headers(req.headers),
|
||||||
|
});
|
||||||
|
|
||||||
return createInnerTRPCContext({ db, user: session?.user, auth });
|
return createInnerTRPCContext({ db, user: session?.user });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createNextApiContext = async (req: NextApiRequest) => {
|
export const createNextApiContext = async (req: NextApiRequest) => {
|
||||||
const db = createDrizzleClient();
|
const db = createDrizzleClient();
|
||||||
const baseAuth = initAuth(db);
|
const auth = initAuth(db);
|
||||||
const headers = new Headers(req.headers as Record<string, string>);
|
|
||||||
const auth = createAuthWithHeaders(baseAuth, headers);
|
|
||||||
|
|
||||||
const session = await auth.api.getSession();
|
const session = await auth.api.getSession({
|
||||||
|
// @ts-expect-error
|
||||||
|
headers: new Headers(req.headers),
|
||||||
|
});
|
||||||
|
|
||||||
return createInnerTRPCContext({ db, user: session?.user, auth });
|
return createInnerTRPCContext({ db, user: session?.user });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createRESTContext = async ({ req }: CreateNextContextOptions) => {
|
export const createRESTContext = async ({ req }: CreateNextContextOptions) => {
|
||||||
const db = createDrizzleClient();
|
const db = createDrizzleClient();
|
||||||
const baseAuth = initAuth(db);
|
const auth = initAuth(db);
|
||||||
const headers = new Headers(req.headers as Record<string, string>);
|
|
||||||
const auth = createAuthWithHeaders(baseAuth, headers);
|
|
||||||
|
|
||||||
let session;
|
let session;
|
||||||
try {
|
try {
|
||||||
session = await auth.api.getSession();
|
session = await auth.api.getSession({
|
||||||
|
// @ts-expect-error
|
||||||
|
headers: new Headers(req.headers),
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting session, ", error);
|
console.error("Error getting session, ", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return createInnerTRPCContext({ db, user: session?.user, auth });
|
return createInnerTRPCContext({ db, user: session?.user });
|
||||||
};
|
};
|
||||||
|
|
||||||
const t = initTRPC
|
const t = initTRPC
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export const socialProvidersPlugin = () => ({
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
},
|
},
|
||||||
async (ctx) =>
|
async (ctx) =>
|
||||||
ctx.json(ctx.context.socialProviders.map((p) => p.id.toLowerCase())),
|
ctx.json(ctx.context.socialProviders.map((p) => p.name.toLowerCase())),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -166,19 +166,9 @@ export const initAuth = (db: dbClient) => {
|
|||||||
databaseHooks: {
|
databaseHooks: {
|
||||||
user: {
|
user: {
|
||||||
create: {
|
create: {
|
||||||
async before(user) {
|
before() {
|
||||||
if (env("NEXT_PUBLIC_DISABLE_SIGN_UP")?.toLowerCase() === "true") {
|
if (env("NEXT_PUBLIC_DISABLE_SIGN_UP")?.toLowerCase() === "true") {
|
||||||
const pendingInvitation = await memberRepo.getByEmailAndStatus(
|
return Promise.resolve(false);
|
||||||
db,
|
|
||||||
user.email,
|
|
||||||
"invited",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!pendingInvitation) {
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(true);
|
|
||||||
}
|
}
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
},
|
},
|
||||||
@@ -191,7 +181,6 @@ export const initAuth = (db: dbClient) => {
|
|||||||
const client = new S3Client({
|
const client = new S3Client({
|
||||||
region: env("S3_REGION") ?? "",
|
region: env("S3_REGION") ?? "",
|
||||||
endpoint: env("S3_ENDPOINT") ?? "",
|
endpoint: env("S3_ENDPOINT") ?? "",
|
||||||
forcePathStyle: env("S3_FORCE_PATH_STYLE") === "true",
|
|
||||||
credentials: {
|
credentials: {
|
||||||
accessKeyId: env("S3_ACCESS_KEY_ID") ?? "",
|
accessKeyId: env("S3_ACCESS_KEY_ID") ?? "",
|
||||||
secretAccessKey: env("S3_SECRET_ACCESS_KEY") ?? "",
|
secretAccessKey: env("S3_SECRET_ACCESS_KEY") ?? "",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { and, asc, desc, eq, inArray, isNull, or } from "drizzle-orm";
|
import { and, asc, desc, eq, exists, inArray, isNull, or, sql } from "drizzle-orm";
|
||||||
|
|
||||||
import type { dbClient } from "@kan/db/client";
|
import type { dbClient } from "@kan/db/client";
|
||||||
import type { BoardVisibilityStatus } from "@kan/db/schema";
|
import type { BoardVisibilityStatus } from "@kan/db/schema";
|
||||||
@@ -481,18 +481,30 @@ export const getWorkspaceAndBoardIdByBoardPublicId = async (
|
|||||||
export const isBoardSlugAvailable = async (
|
export const isBoardSlugAvailable = async (
|
||||||
db: dbClient,
|
db: dbClient,
|
||||||
boardSlug: string,
|
boardSlug: string,
|
||||||
workspaceId: number,
|
boardPublicId: string,
|
||||||
) => {
|
) => {
|
||||||
const result = await db.query.boards.findFirst({
|
const result = await db
|
||||||
columns: {
|
.select({ id: boards.id })
|
||||||
id: true,
|
.from(boards)
|
||||||
},
|
.where(
|
||||||
where: and(
|
and(
|
||||||
eq(boards.slug, boardSlug),
|
eq(boards.publicId, boardPublicId),
|
||||||
eq(boards.workspaceId, workspaceId),
|
exists(
|
||||||
isNull(boards.deletedAt),
|
db
|
||||||
),
|
.select({ id: boards.id })
|
||||||
});
|
.from(boards)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(boards.slug, boardSlug),
|
||||||
|
eq(boards.workspaceId, sql`${boards.workspaceId}`), // Reference outer query's workspaceId
|
||||||
|
isNull(boards.deletedAt),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
return result === undefined;
|
return result.length === 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,12 +23,7 @@ export const create = async (
|
|||||||
createdBy: labelInput.createdBy,
|
createdBy: labelInput.createdBy,
|
||||||
boardId: labelInput.boardId,
|
boardId: labelInput.boardId,
|
||||||
})
|
})
|
||||||
.returning({
|
.returning({ id: labels.id });
|
||||||
id: labels.id,
|
|
||||||
publicId: labels.publicId,
|
|
||||||
name: labels.name,
|
|
||||||
colourCode: labels.colourCode,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (labelInput.cardId && result) {
|
if (labelInput.cardId && result) {
|
||||||
await db.insert(cardsToLabels).values({
|
await db.insert(cardsToLabels).values({
|
||||||
@@ -96,7 +91,6 @@ export const update = async (
|
|||||||
.where(eq(labels.publicId, labelInput.labelPublicId))
|
.where(eq(labels.publicId, labelInput.labelPublicId))
|
||||||
.returning({
|
.returning({
|
||||||
id: labels.id,
|
id: labels.id,
|
||||||
publicId: labels.publicId,
|
|
||||||
name: labels.name,
|
name: labels.name,
|
||||||
colourCode: labels.colourCode,
|
colourCode: labels.colourCode,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -68,20 +68,6 @@ export const create = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const bulkCreate = async (
|
|
||||||
db: dbClient,
|
|
||||||
listInput: {
|
|
||||||
publicId: string;
|
|
||||||
name: string;
|
|
||||||
createdBy: string;
|
|
||||||
boardId: number;
|
|
||||||
index: number;
|
|
||||||
importId?: number;
|
|
||||||
}[],
|
|
||||||
) => {
|
|
||||||
return db.insert(lists).values(listInput).returning();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getByPublicId = async (db: dbClient, listPublicId: string) => {
|
export const getByPublicId = async (db: dbClient, listPublicId: string) => {
|
||||||
return db.query.lists.findFirst({
|
return db.query.lists.findFirst({
|
||||||
columns: {
|
columns: {
|
||||||
|
|||||||
@@ -41,20 +41,6 @@ export const getByPublicId = async (db: dbClient, publicId: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getByEmailAndStatus = async (
|
|
||||||
db: dbClient,
|
|
||||||
email: string,
|
|
||||||
status: MemberStatus,
|
|
||||||
) => {
|
|
||||||
return db.query.workspaceMembers.findFirst({
|
|
||||||
where: and(
|
|
||||||
eq(workspaceMembers.email, email),
|
|
||||||
eq(workspaceMembers.status, status),
|
|
||||||
isNull(workspaceMembers.deletedAt),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const acceptInvite = async (
|
export const acceptInvite = async (
|
||||||
db: dbClient,
|
db: dbClient,
|
||||||
args: { memberId: number; userId: string },
|
args: { memberId: number; userId: string },
|
||||||
|
|||||||
156
pnpm-lock.yaml
generated
@@ -10,14 +10,14 @@ catalogs:
|
|||||||
specifier: ^5.59.15
|
specifier: ^5.59.15
|
||||||
version: 5.62.7
|
version: 5.62.7
|
||||||
'@trpc/client':
|
'@trpc/client':
|
||||||
specifier: ^11.4.3
|
specifier: ^11.0.0-rc.477
|
||||||
version: 11.4.3
|
version: 11.0.0-rc.660
|
||||||
'@trpc/react-query':
|
'@trpc/react-query':
|
||||||
specifier: ^11.4.3
|
specifier: ^11.0.0-rc.477
|
||||||
version: 11.4.3
|
version: 11.0.0-rc.660
|
||||||
'@trpc/server':
|
'@trpc/server':
|
||||||
specifier: ^11.4.3
|
specifier: ^11.0.0-rc.477
|
||||||
version: 11.4.3
|
version: 11.0.0-rc.660
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.12.0
|
specifier: ^9.12.0
|
||||||
version: 9.16.0
|
version: 9.16.0
|
||||||
@@ -140,16 +140,16 @@ importers:
|
|||||||
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)
|
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)
|
||||||
'@trpc/client':
|
'@trpc/client':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 11.4.3(@trpc/server@11.4.3(typescript@5.7.2))(typescript@5.7.2)
|
version: 11.0.0-rc.660(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(typescript@5.7.2)
|
||||||
'@trpc/next':
|
'@trpc/next':
|
||||||
specifier: ^11.0.0-rc.660
|
specifier: ^11.0.0-rc.660
|
||||||
version: 11.0.0-rc.660(@tanstack/react-query@5.62.7(react@18.3.1))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.4.3(@tanstack/react-query@5.62.7(react@18.3.1))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.3(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(@trpc/server@11.4.3(typescript@5.7.2))(next@15.3.4(@babel/core@7.24.5)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)
|
version: 11.0.0-rc.660(@tanstack/react-query@5.62.7(react@18.3.1))(@trpc/client@11.0.0-rc.660(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.0.0-rc.660(@tanstack/react-query@5.62.7(react@18.3.1))(@trpc/client@11.0.0-rc.660(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(next@15.3.4(@babel/core@7.24.5)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)
|
||||||
'@trpc/react-query':
|
'@trpc/react-query':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 11.4.3(@tanstack/react-query@5.62.7(react@18.3.1))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.3(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)
|
version: 11.0.0-rc.660(@tanstack/react-query@5.62.7(react@18.3.1))(@trpc/client@11.0.0-rc.660(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)
|
||||||
'@trpc/server':
|
'@trpc/server':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 11.4.3(typescript@5.7.2)
|
version: 11.0.0-rc.660(typescript@5.7.2)
|
||||||
aws-sdk:
|
aws-sdk:
|
||||||
specifier: ^2.1692.0
|
specifier: ^2.1692.0
|
||||||
version: 2.1692.0
|
version: 2.1692.0
|
||||||
@@ -291,13 +291,13 @@ importers:
|
|||||||
version: link:../stripe
|
version: link:../stripe
|
||||||
'@trpc/server':
|
'@trpc/server':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 11.4.3(typescript@5.7.2)
|
version: 11.0.0-rc.660(typescript@5.7.2)
|
||||||
superjson:
|
superjson:
|
||||||
specifier: 2.2.1
|
specifier: 2.2.1
|
||||||
version: 2.2.1
|
version: 2.2.1
|
||||||
trpc-to-openapi:
|
trpc-to-openapi:
|
||||||
specifier: ^2.3.2
|
specifier: ^2.1.0
|
||||||
version: 2.3.2(@trpc/server@11.4.3(typescript@5.7.2))(zod-openapi@4.1.0(zod@3.24.0))(zod@3.24.0)
|
version: 2.1.0(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(zod-openapi@4.1.0(zod@3.24.0))(zod@3.24.0)
|
||||||
zod:
|
zod:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 3.24.0
|
version: 3.24.0
|
||||||
@@ -2818,11 +2818,11 @@ packages:
|
|||||||
'@tootallnate/quickjs-emscripten@0.23.0':
|
'@tootallnate/quickjs-emscripten@0.23.0':
|
||||||
resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
|
resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
|
||||||
|
|
||||||
'@trpc/client@11.4.3':
|
'@trpc/client@11.0.0-rc.660':
|
||||||
resolution: {integrity: sha512-i2suttUCfColktXT8bqex5kHW5jpT15nwUh0hGSDiW1keN621kSUQKcLJ095blqQAUgB+lsmgSqSMmB4L9shQQ==}
|
resolution: {integrity: sha512-bNpkZEfyMGKHynYFxdLpY8nJ1n7E3JHKcd4Pe2cagmpkzOEF9tFT3kzNf+eLI8XMG8196lTRR0J0W2/1Q8/cug==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@trpc/server': 11.4.3
|
'@trpc/server': 11.0.0-rc.660+74625d5e4
|
||||||
typescript: '>=5.7.2'
|
typescript: '>=5.6.2'
|
||||||
|
|
||||||
'@trpc/next@11.0.0-rc.660':
|
'@trpc/next@11.0.0-rc.660':
|
||||||
resolution: {integrity: sha512-LQHdiVED8h+CJEAy/X9PDmY3fM8VPdZ+3bVkbmEiP4xoNnSKRTbSbknFyRzG9M2AogfIafF4zAZVwOvCxexyGw==}
|
resolution: {integrity: sha512-LQHdiVED8h+CJEAy/X9PDmY3fM8VPdZ+3bVkbmEiP4xoNnSKRTbSbknFyRzG9M2AogfIafF4zAZVwOvCxexyGw==}
|
||||||
@@ -2841,20 +2841,20 @@ packages:
|
|||||||
'@trpc/react-query':
|
'@trpc/react-query':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@trpc/react-query@11.4.3':
|
'@trpc/react-query@11.0.0-rc.660':
|
||||||
resolution: {integrity: sha512-z+jhAiOBD22NNhHtvF0iFp9hO36YFA7M8AiUu/XtNmMxyLd3Y9/d1SMjMwlTdnGqxEGPo41VEWBrdhDUGtUuHg==}
|
resolution: {integrity: sha512-U2BHtYVt+8jt0a8Nrrk5cep8O1UZRxtTCBHtXie9kmJyQWWml43KfHxL5ssnFywaFrDZQz6Ec7kIoOxR/CQNfg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tanstack/react-query': ^5.80.3
|
'@tanstack/react-query': ^5.59.15
|
||||||
'@trpc/client': 11.4.3
|
'@trpc/client': 11.0.0-rc.660+74625d5e4
|
||||||
'@trpc/server': 11.4.3
|
'@trpc/server': 11.0.0-rc.660+74625d5e4
|
||||||
react: '>=18.2.0'
|
react: '>=18.2.0'
|
||||||
react-dom: '>=18.2.0'
|
react-dom: '>=18.2.0'
|
||||||
typescript: '>=5.7.2'
|
typescript: '>=5.6.2'
|
||||||
|
|
||||||
'@trpc/server@11.4.3':
|
'@trpc/server@11.0.0-rc.660':
|
||||||
resolution: {integrity: sha512-wnWq3wiLlMOlYkaIZz+qbuYA5udPTLS4GVVRyFkr6aT83xpdCHyVtURT+u4hSoIrOXQM9OPCNXSXsAujWZDdaw==}
|
resolution: {integrity: sha512-QUapcZCNOpHT7ng9LceGc9ImkboWd0Go9ryrduZpL+p4jdfaC6409AQ3x4XEW6Wu3yBmZAn4CywCsDrDhjDy/w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=5.7.2'
|
typescript: '>=5.6.2'
|
||||||
|
|
||||||
'@tsconfig/node10@1.0.11':
|
'@tsconfig/node10@1.0.11':
|
||||||
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
|
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
|
||||||
@@ -3575,6 +3575,10 @@ packages:
|
|||||||
config-chain@1.1.13:
|
config-chain@1.1.13:
|
||||||
resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
|
resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
|
||||||
|
|
||||||
|
consola@3.2.3:
|
||||||
|
resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==}
|
||||||
|
engines: {node: ^14.18.0 || >=16.10.0}
|
||||||
|
|
||||||
constant-case@2.0.0:
|
constant-case@2.0.0:
|
||||||
resolution: {integrity: sha512-eS0N9WwmjTqrOmR3o83F5vW8Z+9R1HnVz3xmzT2PMFug9ly+Au/fxRWlEBSb6LcZwspSsEn9Xs1uw9YgzAg1EQ==}
|
resolution: {integrity: sha512-eS0N9WwmjTqrOmR3o83F5vW8Z+9R1HnVz3xmzT2PMFug9ly+Au/fxRWlEBSb6LcZwspSsEn9Xs1uw9YgzAg1EQ==}
|
||||||
|
|
||||||
@@ -3625,8 +3629,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
crossws@0.3.5:
|
crossws@0.3.1:
|
||||||
resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==}
|
resolution: {integrity: sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==}
|
||||||
|
|
||||||
css-box-model@1.2.1:
|
css-box-model@1.2.1:
|
||||||
resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==}
|
resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==}
|
||||||
@@ -4440,8 +4444,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
|
resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
|
|
||||||
h3@1.15.1:
|
h3@1.13.0:
|
||||||
resolution: {integrity: sha512-+ORaOBttdUm1E2Uu/obAyCguiI7MbBvsLTndc3gyK3zU+SYLoZXlyCP9Xgy0gikkGufFLTZXCXD6+4BsufnmHA==}
|
resolution: {integrity: sha512-vFEAu/yf8UMUcB4s43OaDaigcqpQd14yanmOsn+NcRX3/guSKncyE2rOYhq8RIchgJrPSs/QiIddnTTR1ddiAg==}
|
||||||
|
|
||||||
handlebars@4.7.8:
|
handlebars@4.7.8:
|
||||||
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
|
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
|
||||||
@@ -4973,6 +4977,9 @@ packages:
|
|||||||
lodash.castarray@4.4.0:
|
lodash.castarray@4.4.0:
|
||||||
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
|
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
|
||||||
|
|
||||||
|
lodash.clonedeep@4.5.0:
|
||||||
|
resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
|
||||||
|
|
||||||
lodash.get@4.4.2:
|
lodash.get@4.4.2:
|
||||||
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
|
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
|
||||||
deprecated: This package is deprecated. Use the optional chaining (?.) operator instead.
|
deprecated: This package is deprecated. Use the optional chaining (?.) operator instead.
|
||||||
@@ -5333,6 +5340,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
mime@3.0.0:
|
||||||
|
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
mimic-fn@2.1.0:
|
mimic-fn@2.1.0:
|
||||||
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -5485,6 +5497,9 @@ packages:
|
|||||||
no-case@2.3.2:
|
no-case@2.3.2:
|
||||||
resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==}
|
resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==}
|
||||||
|
|
||||||
|
node-fetch-native@1.6.4:
|
||||||
|
resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||||
engines: {node: 4.x || >=6.0.0}
|
engines: {node: 4.x || >=6.0.0}
|
||||||
@@ -5498,9 +5513,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
node-mock-http@1.0.1:
|
|
||||||
resolution: {integrity: sha512-0gJJgENizp4ghds/Ywu2FCmcRsgBTmRQzYPZm61wy+Em2sBarSka0OhQS5huLBg6od1zkNpnWMCZloQDFVvOMQ==}
|
|
||||||
|
|
||||||
node-plop@0.26.3:
|
node-plop@0.26.3:
|
||||||
resolution: {integrity: sha512-Cov028YhBZ5aB7MdMWJEmwyBig43aGL5WT4vdoB28Oitau1zZAcHUn8Sgfk9HM33TqhtLJ9PlM/O0Mv+QpV/4Q==}
|
resolution: {integrity: sha512-Cov028YhBZ5aB7MdMWJEmwyBig43aGL5WT4vdoB28Oitau1zZAcHUn8Sgfk9HM33TqhtLJ9PlM/O0Mv+QpV/4Q==}
|
||||||
engines: {node: '>=8.9.4'}
|
engines: {node: '>=8.9.4'}
|
||||||
@@ -5564,6 +5576,9 @@ packages:
|
|||||||
obuf@1.1.2:
|
obuf@1.1.2:
|
||||||
resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
|
resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
|
||||||
|
|
||||||
|
ohash@1.1.4:
|
||||||
|
resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==}
|
||||||
|
|
||||||
on-exit-leak-free@2.1.2:
|
on-exit-leak-free@2.1.2:
|
||||||
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
@@ -5582,8 +5597,8 @@ packages:
|
|||||||
openapi-types@12.1.3:
|
openapi-types@12.1.3:
|
||||||
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
|
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
|
||||||
|
|
||||||
openapi3-ts@4.4.0:
|
openapi3-ts@4.3.3:
|
||||||
resolution: {integrity: sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==}
|
resolution: {integrity: sha512-LKkzBGJcZ6wdvkKGMoSvpK+0cbN5Xc3XuYkJskO+vjEQWJgs1kgtyUk0pjf8KwPuysv323Er62F5P17XQl96Qg==}
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||||
@@ -6721,12 +6736,12 @@ packages:
|
|||||||
trough@2.2.0:
|
trough@2.2.0:
|
||||||
resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
|
resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
|
||||||
|
|
||||||
trpc-to-openapi@2.3.2:
|
trpc-to-openapi@2.1.0:
|
||||||
resolution: {integrity: sha512-uDZou+5a+H77yhiqfzVRBXzTSX8/mQUGeFpr8+VtZmpzUBOmv5XYbyEtWpG7fOdGWZkB99wuuKeWUwNwwOGaXQ==}
|
resolution: {integrity: sha512-LODbI2a9LQamLpEz9hsqeMJBxpNT8KqfyICJhTVe8JBPZM8DvUkoICPlsdHf1KJVteoem/LlbwkD7loKYPWP1Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@trpc/server': ^11.1.0
|
'@trpc/server': ^11.0.0-rc.648
|
||||||
zod: ^3.23.8
|
zod: ^3.23.8
|
||||||
zod-openapi: 4.2.4
|
zod-openapi: ^4.1.0
|
||||||
|
|
||||||
ts-api-utils@1.4.3:
|
ts-api-utils@1.4.3:
|
||||||
resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
|
resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
|
||||||
@@ -6859,6 +6874,9 @@ packages:
|
|||||||
undici-types@6.19.8:
|
undici-types@6.19.8:
|
||||||
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
|
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
|
||||||
|
|
||||||
|
unenv@1.10.0:
|
||||||
|
resolution: {integrity: sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==}
|
||||||
|
|
||||||
unified@10.1.2:
|
unified@10.1.2:
|
||||||
resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==}
|
resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==}
|
||||||
|
|
||||||
@@ -9691,33 +9709,33 @@ snapshots:
|
|||||||
|
|
||||||
'@tootallnate/quickjs-emscripten@0.23.0': {}
|
'@tootallnate/quickjs-emscripten@0.23.0': {}
|
||||||
|
|
||||||
'@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.7.2))(typescript@5.7.2)':
|
'@trpc/client@11.0.0-rc.660(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(typescript@5.7.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@trpc/server': 11.4.3(typescript@5.7.2)
|
'@trpc/server': 11.0.0-rc.660(typescript@5.7.2)
|
||||||
typescript: 5.7.2
|
typescript: 5.7.2
|
||||||
|
|
||||||
'@trpc/next@11.0.0-rc.660(@tanstack/react-query@5.62.7(react@18.3.1))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.4.3(@tanstack/react-query@5.62.7(react@18.3.1))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.3(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(@trpc/server@11.4.3(typescript@5.7.2))(next@15.3.4(@babel/core@7.24.5)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)':
|
'@trpc/next@11.0.0-rc.660(@tanstack/react-query@5.62.7(react@18.3.1))(@trpc/client@11.0.0-rc.660(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.0.0-rc.660(@tanstack/react-query@5.62.7(react@18.3.1))(@trpc/client@11.0.0-rc.660(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(next@15.3.4(@babel/core@7.24.5)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@trpc/client': 11.4.3(@trpc/server@11.4.3(typescript@5.7.2))(typescript@5.7.2)
|
'@trpc/client': 11.0.0-rc.660(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(typescript@5.7.2)
|
||||||
'@trpc/server': 11.4.3(typescript@5.7.2)
|
'@trpc/server': 11.0.0-rc.660(typescript@5.7.2)
|
||||||
next: 15.3.4(@babel/core@7.24.5)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
next: 15.3.4(@babel/core@7.24.5)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
typescript: 5.7.2
|
typescript: 5.7.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@tanstack/react-query': 5.62.7(react@18.3.1)
|
'@tanstack/react-query': 5.62.7(react@18.3.1)
|
||||||
'@trpc/react-query': 11.4.3(@tanstack/react-query@5.62.7(react@18.3.1))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.3(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)
|
'@trpc/react-query': 11.0.0-rc.660(@tanstack/react-query@5.62.7(react@18.3.1))(@trpc/client@11.0.0-rc.660(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)
|
||||||
|
|
||||||
'@trpc/react-query@11.4.3(@tanstack/react-query@5.62.7(react@18.3.1))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.3(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)':
|
'@trpc/react-query@11.0.0-rc.660(@tanstack/react-query@5.62.7(react@18.3.1))(@trpc/client@11.0.0-rc.660(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tanstack/react-query': 5.62.7(react@18.3.1)
|
'@tanstack/react-query': 5.62.7(react@18.3.1)
|
||||||
'@trpc/client': 11.4.3(@trpc/server@11.4.3(typescript@5.7.2))(typescript@5.7.2)
|
'@trpc/client': 11.0.0-rc.660(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(typescript@5.7.2)
|
||||||
'@trpc/server': 11.4.3(typescript@5.7.2)
|
'@trpc/server': 11.0.0-rc.660(typescript@5.7.2)
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
typescript: 5.7.2
|
typescript: 5.7.2
|
||||||
|
|
||||||
'@trpc/server@11.4.3(typescript@5.7.2)':
|
'@trpc/server@11.0.0-rc.660(typescript@5.7.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.7.2
|
typescript: 5.7.2
|
||||||
|
|
||||||
@@ -10597,6 +10615,8 @@ snapshots:
|
|||||||
ini: 1.3.8
|
ini: 1.3.8
|
||||||
proto-list: 1.2.4
|
proto-list: 1.2.4
|
||||||
|
|
||||||
|
consola@3.2.3: {}
|
||||||
|
|
||||||
constant-case@2.0.0:
|
constant-case@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
snake-case: 2.1.0
|
snake-case: 2.1.0
|
||||||
@@ -10648,7 +10668,7 @@ snapshots:
|
|||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
crossws@0.3.5:
|
crossws@0.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
uncrypto: 0.1.3
|
uncrypto: 0.1.3
|
||||||
|
|
||||||
@@ -11622,17 +11642,18 @@ snapshots:
|
|||||||
section-matter: 1.0.0
|
section-matter: 1.0.0
|
||||||
strip-bom-string: 1.0.0
|
strip-bom-string: 1.0.0
|
||||||
|
|
||||||
h3@1.15.1:
|
h3@1.13.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
cookie-es: 1.2.2
|
cookie-es: 1.2.2
|
||||||
crossws: 0.3.5
|
crossws: 0.3.1
|
||||||
defu: 6.1.4
|
defu: 6.1.4
|
||||||
destr: 2.0.3
|
destr: 2.0.3
|
||||||
iron-webcrypto: 1.2.1
|
iron-webcrypto: 1.2.1
|
||||||
node-mock-http: 1.0.1
|
ohash: 1.1.4
|
||||||
radix3: 1.1.2
|
radix3: 1.1.2
|
||||||
ufo: 1.5.4
|
ufo: 1.5.4
|
||||||
uncrypto: 0.1.3
|
uncrypto: 0.1.3
|
||||||
|
unenv: 1.10.0
|
||||||
|
|
||||||
handlebars@4.7.8:
|
handlebars@4.7.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -12174,6 +12195,8 @@ snapshots:
|
|||||||
|
|
||||||
lodash.castarray@4.4.0: {}
|
lodash.castarray@4.4.0: {}
|
||||||
|
|
||||||
|
lodash.clonedeep@4.5.0: {}
|
||||||
|
|
||||||
lodash.get@4.4.2: {}
|
lodash.get@4.4.2: {}
|
||||||
|
|
||||||
lodash.isplainobject@4.0.6: {}
|
lodash.isplainobject@4.0.6: {}
|
||||||
@@ -12915,6 +12938,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mime-db: 1.52.0
|
mime-db: 1.52.0
|
||||||
|
|
||||||
|
mime@3.0.0: {}
|
||||||
|
|
||||||
mimic-fn@2.1.0: {}
|
mimic-fn@2.1.0: {}
|
||||||
|
|
||||||
mini-svg-data-uri@1.4.4: {}
|
mini-svg-data-uri@1.4.4: {}
|
||||||
@@ -13051,6 +13076,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lower-case: 1.1.4
|
lower-case: 1.1.4
|
||||||
|
|
||||||
|
node-fetch-native@1.6.4: {}
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url: 5.0.0
|
whatwg-url: 5.0.0
|
||||||
@@ -13058,8 +13085,6 @@ snapshots:
|
|||||||
node-gyp-build@4.8.4:
|
node-gyp-build@4.8.4:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
node-mock-http@1.0.1: {}
|
|
||||||
|
|
||||||
node-plop@0.26.3:
|
node-plop@0.26.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime-corejs3': 7.26.0
|
'@babel/runtime-corejs3': 7.26.0
|
||||||
@@ -13130,6 +13155,8 @@ snapshots:
|
|||||||
|
|
||||||
obuf@1.1.2: {}
|
obuf@1.1.2: {}
|
||||||
|
|
||||||
|
ohash@1.1.4: {}
|
||||||
|
|
||||||
on-exit-leak-free@2.1.2: {}
|
on-exit-leak-free@2.1.2: {}
|
||||||
|
|
||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
@@ -13148,7 +13175,7 @@ snapshots:
|
|||||||
|
|
||||||
openapi-types@12.1.3: {}
|
openapi-types@12.1.3: {}
|
||||||
|
|
||||||
openapi3-ts@4.4.0:
|
openapi3-ts@4.3.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
yaml: 2.6.1
|
yaml: 2.6.1
|
||||||
|
|
||||||
@@ -14491,12 +14518,13 @@ snapshots:
|
|||||||
|
|
||||||
trough@2.2.0: {}
|
trough@2.2.0: {}
|
||||||
|
|
||||||
trpc-to-openapi@2.3.2(@trpc/server@11.4.3(typescript@5.7.2))(zod-openapi@4.1.0(zod@3.24.0))(zod@3.24.0):
|
trpc-to-openapi@2.1.0(@trpc/server@11.0.0-rc.660(typescript@5.7.2))(zod-openapi@4.1.0(zod@3.24.0))(zod@3.24.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@trpc/server': 11.4.3(typescript@5.7.2)
|
'@trpc/server': 11.0.0-rc.660(typescript@5.7.2)
|
||||||
co-body: 6.2.0
|
co-body: 6.2.0
|
||||||
h3: 1.15.1
|
h3: 1.13.0
|
||||||
openapi3-ts: 4.4.0
|
lodash.clonedeep: 4.5.0
|
||||||
|
openapi3-ts: 4.3.3
|
||||||
zod: 3.24.0
|
zod: 3.24.0
|
||||||
zod-openapi: 4.1.0(zod@3.24.0)
|
zod-openapi: 4.1.0(zod@3.24.0)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -14640,6 +14668,14 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@6.19.8: {}
|
undici-types@6.19.8: {}
|
||||||
|
|
||||||
|
unenv@1.10.0:
|
||||||
|
dependencies:
|
||||||
|
consola: 3.2.3
|
||||||
|
defu: 6.1.4
|
||||||
|
mime: 3.0.0
|
||||||
|
node-fetch-native: 1.6.4
|
||||||
|
pathe: 1.1.2
|
||||||
|
|
||||||
unified@10.1.2:
|
unified@10.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 2.0.11
|
'@types/unist': 2.0.11
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ packages:
|
|||||||
|
|
||||||
catalog:
|
catalog:
|
||||||
"@tanstack/react-query": ^5.59.15
|
"@tanstack/react-query": ^5.59.15
|
||||||
"@trpc/client": ^11.4.3
|
"@trpc/client": ^11.0.0-rc.477
|
||||||
"@trpc/react-query": ^11.4.3
|
"@trpc/react-query": ^11.0.0-rc.477
|
||||||
"@trpc/server": ^11.4.3
|
"@trpc/server": ^11.0.0-rc.477
|
||||||
eslint: ^9.12.0
|
eslint: ^9.12.0
|
||||||
prettier: ^3.3.3
|
prettier: ^3.3.3
|
||||||
tailwindcss: ^3.4.14
|
tailwindcss: ^3.4.14
|
||||||
|
|||||||
@@ -106,7 +106,6 @@
|
|||||||
"S3_ACCESS_KEY_ID",
|
"S3_ACCESS_KEY_ID",
|
||||||
"S3_SECRET_ACCESS_KEY",
|
"S3_SECRET_ACCESS_KEY",
|
||||||
"S3_ENDPOINT",
|
"S3_ENDPOINT",
|
||||||
"S3_FORCE_PATH_STYLE",
|
|
||||||
"PORT",
|
"PORT",
|
||||||
"BETTER_AUTH_SECRET",
|
"BETTER_AUTH_SECRET",
|
||||||
"BETTER_AUTH_TRUSTED_ORIGINS"
|
"BETTER_AUTH_TRUSTED_ORIGINS"
|
||||||
|
|||||||