Compare commits

..

7 Commits

Author SHA1 Message Date
Henry
4ac89ba8c0 Fix build and add dyanmic imports 2025-06-25 17:45:38 +01:00
Henry
a2f7582999 Extend lang support 2025-06-25 13:20:07 +01:00
Henry
8a93951464 Update lang support 2025-06-25 12:12:47 +01:00
Henry
bf2db74528 Extend language support 2025-06-24 22:41:06 +01:00
Henry
f564b52159 Add missing translations and tweak selector styling 2025-06-24 22:15:32 +01:00
Henry
2ef2493bb5 Fix hydration and locale issues 2025-06-24 21:37:03 +01:00
Henry
f105e951e6 feat: setup localisation with lingui 2025-06-23 23:43:16 +01:00
87 changed files with 1321 additions and 4210 deletions

View File

@@ -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
View File

@@ -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 @@
} }
} }
] ]
} }

View File

@@ -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

View File

@@ -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` |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 433 KiB

View File

@@ -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

View File

@@ -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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 433 KiB

View File

@@ -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 />}

View File

@@ -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>
</> </>

View File

@@ -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>
); );

View 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;

View File

@@ -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>
);
}

View File

@@ -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>
)} )}

View File

@@ -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" }}
/> />
); );

View File

@@ -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,

View File

@@ -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>
); );
}; };

View File

@@ -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"

View File

@@ -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>
); );
}; };

View File

@@ -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>
</> </>
); );

View File

@@ -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

View File

@@ -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">

View File

@@ -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(),
}, },

View File

@@ -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,
);
}

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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)");
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -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: ["/"],
};

View File

@@ -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>

View File

@@ -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",
}), }),
); );

View File

@@ -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";

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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%;

View File

@@ -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;

View File

@@ -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`}

View File

@@ -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 ?? ""} />
)} )}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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`}

View File

@@ -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>
);
}

View File

@@ -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 />}

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 }) => (

View File

@@ -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...`}

View File

@@ -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} />
)} )}

View File

@@ -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">

View File

@@ -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 />}

View File

@@ -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">

View File

@@ -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);

View File

@@ -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 />

View File

@@ -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}

View File

@@ -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}

View File

@@ -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": {

View File

@@ -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,
};
}),
}); });

View File

@@ -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;

View File

@@ -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: {

View File

@@ -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({

View File

@@ -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;
}), }),

View File

@@ -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

View File

@@ -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") ?? "",

View File

@@ -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;
}; };

View File

@@ -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,
}); });

View File

@@ -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: {

View File

@@ -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
View File

@@ -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

View File

@@ -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

View File

@@ -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"