Extend lang support
This commit is contained in:
@@ -1,60 +1,128 @@
|
|||||||
version: 1
|
version: 1
|
||||||
checksums:
|
checksums:
|
||||||
8033ed03a24d7c4898c90a5365297c20:
|
8033ed03a24d7c4898c90a5365297c20:
|
||||||
|
"%20(edited)/singular": 666ee7e6d86957a87c23fa12b21b773f
|
||||||
"%2C%20or%20see%20our/singular": e8a8f2a078fcaea03481710dcaccce5d
|
"%2C%20or%20see%20our/singular": e8a8f2a078fcaea03481710dcaccce5d
|
||||||
|
"%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
|
"%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
|
||||||
|
Activity/singular: 1948763de8e531483a798b68195e297e
|
||||||
Activity%20logs/singular: 8b1f0bb96a905646ecfad1cfdfa42168
|
Activity%20logs/singular: 8b1f0bb96a905646ecfad1cfdfa42168
|
||||||
Add%20a%20card/singular: 9d80becc4fe65a74e0004e4fd290735d
|
Add%20a%20card/singular: 9d80becc4fe65a74e0004e4fd290735d
|
||||||
|
Add%20a%20comment.../singular: c28cfccc0cc69a2a2487e37eb9988028
|
||||||
Add%20description...%20(type%20'%2F'%20to%20open%20commands)/singular: df99ea599521682e9ad0a90716955dc6
|
Add%20description...%20(type%20'%2F'%20to%20open%20commands)/singular: df99ea599521682e9ad0a90716955dc6
|
||||||
|
Add%20label/singular: 0be732d46df263265935fda342097a08
|
||||||
|
Add%20member/singular: 11979625770516ca287e929381778e02
|
||||||
|
added%20a%20label%20to%20the%20card/singular: 04c5e4ee9b7b7e77ef089d2ac473d235
|
||||||
|
added%20a%20member%20to%20the%20card/singular: 95495e770062e2fd590333d56e6308d2
|
||||||
|
added%20label%20%3C0%3E%7Blabel%7D%3C%2F0%3E/singular: 1caba6b6bca4c6fa1ab173d241059dc2
|
||||||
Admin%20roles/singular: 32a5d78073b9bb9a246773afba8831df
|
Admin%20roles/singular: 32a5d78073b9bb9a246773afba8831df
|
||||||
All%20seats%20are%20free%20during%20the%20beta%20until%20Sept%201st!/singular: 015cec3899cf99e37c3cf231d432e31d
|
All%20seats%20are%20free%20during%20the%20beta%20until%20Sept%201st!/singular: 015cec3899cf99e37c3cf231d432e31d
|
||||||
All%20systems%20operational/singular: ee943a4046b09e6334cceeea9fda2bfc
|
All%20systems%20operational/singular: ee943a4046b09e6334cceeea9fda2bfc
|
||||||
|
Already%20have%20an%20account%3F%20%3C0%3E%3C1%3ESign%20in%3C%2F1%3E%3C%2F0%3E/singular: 2959fd276248208b65cb27ed46b20135
|
||||||
|
An%20error%20occurred%20while%20disconnecting%20your%20Trello%20account./singular: 0aa3973b860c1faf8d9123aebf567e40
|
||||||
|
API%20keys/singular: 07f3620a30e08136f0b072c9af8d1eef
|
||||||
API%20Reference/singular: 7dcc877064bfdf889ec55dbc9e06a242
|
API%20Reference/singular: 7dcc877064bfdf889ec55dbc9e06a242
|
||||||
|
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%20this%20card%3F/singular: 131a5a7cccca7be8b32c987d9fa0b0c0
|
||||||
|
Are%20you%20sure%20you%20want%20to%20delete%20this%20comment%3F/singular: 55fe6f1b52b71b91b2a079cfda3cc7fe
|
||||||
|
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
|
||||||
|
Billing/singular: b01dbdd049ebbd4a349fa64d6ce65a3b
|
||||||
|
Billing%20portal/singular: dc3afa14ffe19f5920992a0c9d49d525
|
||||||
Board/singular: 101ff39aab674c033c15ab978d0d8bac
|
Board/singular: 101ff39aab674c033c15ab978d0d8bac
|
||||||
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%20cannot%20exceed%2060%20characters/singular: 8fd3417559a56aa42d297b14d480c382
|
||||||
|
Board%20URL%20copied%20to%20clipboard/singular: 5ef04fee9c8e5299a2fd1ad223eb95c9
|
||||||
|
Board%20URL%20must%20be%20at%20least%203%20characters%20long/singular: 105f5b4f3b606baf7b5302117022bd68
|
||||||
Board%20visibility/singular: b4205f3058297833a626156057c24b26
|
Board%20visibility/singular: b4205f3058297833a626156057c24b26
|
||||||
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
|
||||||
|
Cancel/singular: 2e2a849c2223911717de8caa2c71bade
|
||||||
|
Card%20not%20found/singular: 91509e2f92b0b3b11330b6983139fdbf
|
||||||
Card%20title/singular: 7c34f59f4005e6cb3a6ff546ea0b96e3
|
Card%20title/singular: 7c34f59f4005e6cb3a6ff546ea0b96e3
|
||||||
|
Change%20the%20language%20of%20the%20app./singular: fb20db145e28ed44aba89c146ded7bfc
|
||||||
|
Check%20your%20inbox/singular: e9a430fcd298def74212238df0f680d6
|
||||||
|
Clear%20filters/singular: 8f40ab5af527e4b190da94e7b6221379
|
||||||
|
Click%20on%20the%20link%20we've%20sent%20to%20%7BmagicLinkRecipient%7D%20to%20sign%20in./singular: 210b6ff8727f976182ec3f29ea3c7667
|
||||||
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%20control%20and%20ownership%3A/singular: 0d8b682ba873272217425ccfc96aa9cd
|
Complete%20control%20and%20ownership%3A/singular: 0d8b682ba873272217425ccfc96aa9cd
|
||||||
|
Confirm%20URL%20change/singular: e14b4ce081d1ef6194b37e1814e97b39
|
||||||
Connect%20Trello/singular: 4440a0b9e387ef7136e3958e7a089213
|
Connect%20Trello/singular: 4440a0b9e387ef7136e3958e7a089213
|
||||||
Connect%20your%20favorite%20tools%20to%20streamline%20your%20workflow./singular: 033c5dcdb059ccb634aef7fe63f2f45d
|
Connect%20your%20favorite%20tools%20to%20streamline%20your%20workflow./singular: 033c5dcdb059ccb634aef7fe63f2f45d
|
||||||
|
Connect%20your%20Trello%20account%20to%20import%20boards./singular: 84a510a7485f43e43f2191bd161171d7
|
||||||
Contact/singular: 9afa39bc47019ee6dec6c74b6273967c
|
Contact/singular: 9afa39bc47019ee6dec6c74b6273967c
|
||||||
Contact%20us/singular: 694a45bc5c96f6f3a27873c1cf5a7707
|
Contact%20us/singular: 694a45bc5c96f6f3a27873c1cf5a7707
|
||||||
|
Continue%20with%20/singular: 8ed03cf7c5e60a6edf3470a4558ff058
|
||||||
|
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
|
||||||
Create%20another/singular: 2de8a82a416eb78c0462aa36278edc9a
|
Create%20another/singular: 2de8a82a416eb78c0462aa36278edc9a
|
||||||
Create%20board/singular: 155b62818bfab0e34f0089e1b34a32f3
|
Create%20board/singular: 155b62818bfab0e34f0089e1b34a32f3
|
||||||
Create%20card/singular: 32792935dd5837a9433909b04021c22b
|
Create%20card/singular: 32792935dd5837a9433909b04021c22b
|
||||||
Create%20label/singular: c64e8180fa956f005edc1fd9b8e65abb
|
Create%20label/singular: c64e8180fa956f005edc1fd9b8e65abb
|
||||||
|
Create%20list/singular: 858c3d514aa95765ec82114393199144
|
||||||
Create%20new%20board/singular: cbb87b1f4cc46b336ad9510c9f19407c
|
Create%20new%20board/singular: cbb87b1f4cc46b336ad9510c9f19407c
|
||||||
|
Create%20new%20key/singular: 236bdf75e42b42ec7fb25639bdc035af
|
||||||
Create%20new%20label/singular: 6be30980847b577847baa01bb5f29711
|
Create%20new%20label/singular: 6be30980847b577847baa01bb5f29711
|
||||||
Create%20new%20list/singular: ce61ca8be7b3f6c26c32b684a42a2c26
|
Create%20new%20list/singular: ce61ca8be7b3f6c26c32b684a42a2c26
|
||||||
Create%20workspace/singular: 2e6718e79964ea5ce22d76c2189c77ca
|
Create%20workspace/singular: 2e6718e79964ea5ce22d76c2189c77ca
|
||||||
|
created%20the%20card/singular: 605475f5aaeb4dbccbf7c4eb9107b43d
|
||||||
Custom%20domain/singular: b09e7a9c187b7163b4a6cfc78042fe42
|
Custom%20domain/singular: b09e7a9c187b7163b4a6cfc78042fe42
|
||||||
|
Custom%20URLs%20are%20a%20premium%20feature.%20You'll%20be%20directed%20to%20upgrade%20your%20account./singular: b52e4f6122cf739c9551d29beb89b25e
|
||||||
Dark/singular: 73e6e208ba628b26e90fcf6dce15e1b2
|
Dark/singular: 73e6e208ba628b26e90fcf6dce15e1b2
|
||||||
Delete/singular: 8bcf303dd10a645b5baacb02b47d72c9
|
Delete/singular: 8bcf303dd10a645b5baacb02b47d72c9
|
||||||
|
Delete%20account/singular: a9d11113f1a1d7e20582bdcc9633bcef
|
||||||
|
Delete%20board/singular: e915cd160651ad4b61a67e8da0737c23
|
||||||
|
Delete%20card/singular: 18811ccf76fbb7a2772d648f7c5f0ea7
|
||||||
|
Delete%20comment/singular: a3dae0c9ddbc73677aad9812e3dffd69
|
||||||
Delete%20list/singular: b694f58e4c973d8797a78e746b550b1f
|
Delete%20list/singular: b694f58e4c973d8797a78e746b550b1f
|
||||||
|
Delete%20workspace/singular: 9017c821209528d9b20d40a488854cdf
|
||||||
|
Disconnect%20Trello/singular: a2dd737d133887d34504728c7e7f05e3
|
||||||
Discuss%20and%20collaborate%20on%20cards./singular: f2e87816709f602130831ead50bba457
|
Discuss%20and%20collaborate%20on%20cards./singular: f2e87816709f602130831ead50bba457
|
||||||
|
Display%20name/singular: 725f331065e9d594d720d07b1ae51a90
|
||||||
|
Display%20name%20cannot%20exceed%20280%20characters/singular: 426ddbcc87590e9d028bdca4e585c549
|
||||||
|
Display%20name%20must%20be%20at%20least%203%20characters%20long/singular: 84178b79845a70937af5e26020403052
|
||||||
|
Display%20name%20updated/singular: 4e27503ca24ef5f009713951ca9e5b97
|
||||||
Do%20you%20offer%20a%20free%20plan%3F/singular: 3ef16b0994f0d9ae17067782ca9bed1b
|
Do%20you%20offer%20a%20free%20plan%3F/singular: 3ef16b0994f0d9ae17067782ca9bed1b
|
||||||
docs/singular: 307bff5678ada918da1f1ec3edc76cac
|
docs/singular: 307bff5678ada918da1f1ec3edc76cac
|
||||||
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
|
||||||
|
Edit%20board%20URL/singular: d8276dfc0189f371ec7d80a2047c07aa
|
||||||
|
Edit%20comment/singular: 7e4b46525fcb6b47b71798e31c46e374
|
||||||
Edit%20label/singular: 0309e0be1512b1e0b0ceb87c69a53d03
|
Edit%20label/singular: 0309e0be1512b1e0b0ceb87c69a53d03
|
||||||
|
email/singular: f31eb214738e037d58e26149797739df
|
||||||
|
Email/singular: e7f34943a0c2fb849db1839ff6ef5cb5
|
||||||
|
Enter%20your%20email%20address/singular: 9bc008365ebe3e404e241c8ca876f56e
|
||||||
|
Enter%20your%20name/singular: cd95fbdd0533f2c2e8edf9d9bd9aa8df
|
||||||
|
Enter%20your%20password/singular: ea4fdd034522dead21bae0c0abb52eae
|
||||||
|
Error%20deleting%20account/singular: d42965a9bc9e5ec4ed57890268924643
|
||||||
|
Error%20deleting%20workspace/singular: 0aec9bd8170bc84f5ea5c9a47c52ed26
|
||||||
|
Error%20disconnecting%20Trello/singular: a8766853864eddc480113778a708871b
|
||||||
|
Error%20updating%20display%20name/singular: 0d7979a25d290123ed8b8906bf468f1a
|
||||||
|
Error%20updating%20profile%20image/singular: 462a56a6e574abc9e13cb48f1266adad
|
||||||
|
Error%20updating%20workspace%20description/singular: 7a9fd7c69b90ab848ee1f1d4a4995381
|
||||||
|
Error%20updating%20workspace%20name/singular: 46025f42a7d5eff689e9cb8bed14a1e4
|
||||||
|
Error%20updating%20workspace%20URL/singular: 9da6c012e1b512d8c7fbd812626c4675
|
||||||
|
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
|
||||||
|
Failed%20to%20login%20with%20%7B0%7D.%20Please%20try%20again./singular: 669a4b4247a73f53fb9b8b16e42d166f
|
||||||
FAQs/singular: dc36d7992372ccd419b39ef51cf5b16c
|
FAQs/singular: dc36d7992372ccd419b39ef51cf5b16c
|
||||||
Features/singular: 341ff316a339b106a178f0b8d362951b
|
Features/singular: 341ff316a339b106a178f0b8d362951b
|
||||||
Feedback/singular: 6fac88806e0c269a30777b283988c61c
|
Feedback/singular: 6fac88806e0c269a30777b283988c61c
|
||||||
Feedback%20sent/singular: 38a490c7ad5f1aa6013dfa879d2c7818
|
Feedback%20sent/singular: 38a490c7ad5f1aa6013dfa879d2c7818
|
||||||
|
Filter/singular: 626325a05e4c8800f7ede7012b0cadaf
|
||||||
Find%20answers%20to%20common%20questions%20about%20the%20project.%20Can't%20find%20what%20you're%20looking%20for%3F%20Feel%20free%20to%20%3C0%3Econtact%20us%3C%2F0%3E./singular: 4d99230dad703bf2d71ba670c2f79d3d
|
Find%20answers%20to%20common%20questions%20about%20the%20project.%20Can't%20find%20what%20you're%20looking%20for%3F%20Feel%20free%20to%20%3C0%3Econtact%20us%3C%2F0%3E./singular: 4d99230dad703bf2d71ba670c2f79d3d
|
||||||
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
|
||||||
@@ -75,6 +143,8 @@ checksums:
|
|||||||
How%20do%20I%20invite%20team%20members%3F/singular: e620622060a06288e86ccda35395db87
|
How%20do%20I%20invite%20team%20members%3F/singular: e620622060a06288e86ccda35395db87
|
||||||
How%20do%20I%20self-host%3F/singular: ec0b6afc8ad2f581b512c0fd82926b75
|
How%20do%20I%20self-host%3F/singular: ec0b6afc8ad2f581b512c0fd82926b75
|
||||||
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%20the%20workspace%20data%20will%20be%20permanently%20deleted%20and%20want%20to%20proceed./singular: fe1183a7439299210d8ab7379b8f4d01
|
||||||
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
|
||||||
@@ -84,15 +154,28 @@ checksums:
|
|||||||
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
|
||||||
Individuals/singular: 83bfc81f20e60556c44ec80c7d0603ec
|
Individuals/singular: 83bfc81f20e60556c44ec80c7d0603ec
|
||||||
Integrations/singular: 0ccce343287704cd90150c32e2fcad36
|
Integrations/singular: 0ccce343287704cd90150c32e2fcad36
|
||||||
|
Invite/singular: 181884cea804cbde665f160811ee7ad0
|
||||||
|
Invite%20member/singular: ade922db1be6b26bc979565ce5de2bc7
|
||||||
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
|
||||||
|
Labels/singular: 6f15627a90002323eac018274b6922d6
|
||||||
Labels%20%26%20Filters/singular: 8f0bdd6084516f8241cb613178114390
|
Labels%20%26%20Filters/singular: 8f0bdd6084516f8241cb613178114390
|
||||||
|
Language/singular: 277fd1a41cc237a437cd1d5e4a80463b
|
||||||
Legal/singular: 00d3f08da61a21887009819cf93fb414
|
Legal/singular: 00d3f08da61a21887009819cf93fb414
|
||||||
License/singular: ac2bcac75151e2fcd9eb768cea275a77
|
License/singular: ac2bcac75151e2fcd9eb768cea275a77
|
||||||
Light/singular: bb78ea2c6edd8662676c81a8a13ecb93
|
Light/singular: bb78ea2c6edd8662676c81a8a13ecb93
|
||||||
|
Link%20copied/singular: 4a4805700b31ec65bcb3ca962bccfd1b
|
||||||
|
List/singular: 94f13e7ef909a4de9db7abaa1f9f0b61
|
||||||
|
List%20name/singular: e925e2e6ccaf0eb4064a888aaea8d3c2
|
||||||
|
Login%20%7C%20kan.bn/singular: 42a6c8dcd73e0d46e652646dc86871eb
|
||||||
Logout/singular: 07948fdf20705e04a7bf68ab197512bf
|
Logout/singular: 07948fdf20705e04a7bf68ab197512bf
|
||||||
|
magic%20link/singular: db9d17a5bd98bba8aa0d8068c68579f3
|
||||||
Members/singular: 0932e80cba1e3e0a7f52bb67ff31da32
|
Members/singular: 0932e80cba1e3e0a7f52bb67ff31da32
|
||||||
|
Members%20%7C%20%7B0%7D/singular: a29e3e9f1076acd178c417d047584e88
|
||||||
Monthly/singular: 818f1192e32bb855597f930d3e78806e
|
Monthly/singular: 818f1192e32bb855597f930d3e78806e
|
||||||
|
moved%20the%20card%20from%20%3C0%3E%7BfromList%7D%3C%2F0%3E%20to%3C1%3E%7BtoList%7D%3C%2F1%3E/singular: c2d0d72ad97510344b51154ef15060e0
|
||||||
|
moved%20the%20card%20to%20another%20list/singular: 2e8c41bfafb42fa299ce56e4300205ff
|
||||||
Name/singular: 9368b5a047572b6051f334af5aa76819
|
Name/singular: 9368b5a047572b6051f334af5aa76819
|
||||||
Need%20help%3F/singular: 04e7322f2d3ffb2d73ff2f64b71637c8
|
Need%20help%3F/singular: 04e7322f2d3ffb2d73ff2f64b71637c8
|
||||||
New/singular: 126d036fae5fb6b629728ecb97e6195b
|
New/singular: 126d036fae5fb6b629728ecb97e6195b
|
||||||
@@ -106,30 +189,56 @@ checksums:
|
|||||||
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
Payment%20frequency/singular: 63ded0e4ffb462ca8bd33d38e4691d86
|
||||||
|
Pending/singular: 030a6f3395d5d4efddd3cc67d6009039
|
||||||
per%20user%2Fmonth/singular: 72af182c1ba6df6732640f4d8a78d360
|
per%20user%2Fmonth/singular: 72af182c1ba6df6732640f4d8a78d360
|
||||||
|
Please%20enter%20a%20valid%20email%20address/singular: 8de4bc8832b11b380bc4cbcedc16e48b
|
||||||
|
Please%20enter%20a%20valid%20name/singular: f2d741f1b5cae722e35cb5206786f932
|
||||||
|
Please%20enter%20a%20valid%20password/singular: 4b32c17e19b79bcbf0bb092c06ba310f
|
||||||
|
Please%20select%20a%20file%20to%20upload./singular: de315bf594047f8ef9307a7fa9285844
|
||||||
Please%20try%20again%20later%2C%20or%20contact%20customer%20support./singular: 21ffcf0b00e7cd7b64f7454a95762e1d
|
Please%20try%20again%20later%2C%20or%20contact%20customer%20support./singular: 21ffcf0b00e7cd7b64f7454a95762e1d
|
||||||
Pricing/singular: ce27f1aeacccc542a174c4b2bce022b0
|
Pricing/singular: ce27f1aeacccc542a174c4b2bce022b0
|
||||||
Priority%20email%20support/singular: 678538c912a770b1e1416ecdb8e299b1
|
Priority%20email%20support/singular: 678538c912a770b1e1416ecdb8e299b1
|
||||||
Privacy%20policy/singular: 462c6a536b52873e4498785c66dd48c8
|
Privacy%20policy/singular: 462c6a536b52873e4498785c66dd48c8
|
||||||
Private/singular: d740d0660e68c61db1853489356030ae
|
Private/singular: d740d0660e68c61db1853489356030ae
|
||||||
|
Profile%20image%20updated/singular: 1ba6609b50bc217a36ab659563e78dae
|
||||||
|
Profile%20picture/singular: d9b5e595877ffe2557486a987dd28274
|
||||||
Public/singular: c834f11dab0881632aded3f01d4f8fa9
|
Public/singular: c834f11dab0881632aded3f01d4f8fa9
|
||||||
Questions%3F/singular: c7c3a47f8b230ce7a43f3c090f21d683
|
Questions%3F/singular: c7c3a47f8b230ce7a43f3c090f21d683
|
||||||
|
Remove/singular: dba2fe5fe9f83f8078c687f28cba4b52
|
||||||
|
Remove%20member/singular: 1d77c2ca3768e486fd1fb5df30cb60d9
|
||||||
|
removed%20a%20label%20from%20the%20card/singular: f83ec18850a2a8a3f8242a740a04df06
|
||||||
|
removed%20a%20member%20from%20the%20card/singular: 0bd7561bb79358fde3d595c3e60a315a
|
||||||
|
removed%20label%20%3C0%3E%7Blabel%7D%3C%2F0%3E/singular: 1e7ebc1866136352bdc3cd90afc083f0
|
||||||
Resources/singular: ec7fb05ed963bb6781a35782b3475502
|
Resources/singular: ec7fb05ed963bb6781a35782b3475502
|
||||||
|
Revoke/singular: be57685a85b6dfeaeb6eab4e9560b520
|
||||||
Roadmap/singular: c60f4a1acf30e566861bf130f13b9ae7
|
Roadmap/singular: c60f4a1acf30e566861bf130f13b9ae7
|
||||||
|
Role/singular: 53743bbb6ca938f5b893552e839d067f
|
||||||
Run%20on%20your%20own%20infrastructure/singular: eba804911562b8dbf9d69c3e27f1d708
|
Run%20on%20your%20own%20infrastructure/singular: eba804911562b8dbf9d69c3e27f1d708
|
||||||
|
Save/singular: f7a2929f33bc420195e59ac5a8bcd454
|
||||||
Save%20time%20with%20reusable%20board%20templates./singular: d0f2d7d0fd682ceaf4ca12c6353fd75a
|
Save%20time%20with%20reusable%20board%20templates./singular: d0f2d7d0fd682ceaf4ca12c6353fd75a
|
||||||
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
|
||||||
Send/singular: 76a8497d7b546628b03bb81d5c1ce995
|
Send/singular: 76a8497d7b546628b03bb81d5c1ce995
|
||||||
Settings/singular: 8df6777277469c1fd88cc18dde2f1cc3
|
Settings/singular: 8df6777277469c1fd88cc18dde2f1cc3
|
||||||
|
Settings%20%7C%20%7B0%7D/singular: b8fc73080bc9c8f4f1403b2a69bd1ac5
|
||||||
Sign%20in/singular: cb8757c7450e17de1e226e82fb0fa4a2
|
Sign%20in/singular: cb8757c7450e17de1e226e82fb0fa4a2
|
||||||
|
Sign%20up%20%7C%20kan.bn/singular: f3de2a110c90358e6eac07d0b2f663a6
|
||||||
|
Sign%20up%20disabled/singular: 9581b1f75b404ac0ecb7e603e0d4189c
|
||||||
|
Sign%20up%20is%20currently%20disabled.%20Please%20try%20again%20later./singular: c6cb7c455ec053b351a27029158ff166
|
||||||
|
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
|
||||||
Star%20on%20Github/singular: e5cb0428f1ac6485688f120d78742875
|
Star%20on%20Github/singular: e5cb0428f1ac6485688f120d78742875
|
||||||
|
Success/singular: c43827becada6750f7a25890905f38b9
|
||||||
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
|
||||||
@@ -140,36 +249,88 @@ checksums:
|
|||||||
The%20open%20source%20%3C0%2F%3E%20alternative%20to%20Trello/singular: 692cb2e8a8e610953c996826beed45a2
|
The%20open%20source%20%3C0%2F%3E%20alternative%20to%20Trello/singular: 692cb2e8a8e610953c996826beed45a2
|
||||||
The%20visibility%20of%20your%20board%20has%20been%20set%20to%20%7B0%7D./singular: 970e17a115f7374e60e0a8ded425db8f
|
The%20visibility%20of%20your%20board%20has%20been%20set%20to%20%7B0%7D./singular: 970e17a115f7374e60e0a8ded425db8f
|
||||||
Theme/singular: 21fe00b7a518089576fb83c08631107a
|
Theme/singular: 21fe00b7a518089576fb83c08631107a
|
||||||
|
They%20won't%20be%20able%20to%20access%20this%20workspace./singular: 93b740350fe3430319fbca85349e41d9
|
||||||
|
This%20action%20can't%20be%20undone./singular: cb222ff89715d8c971e8c25d121e1dbd
|
||||||
|
This%20board%20URL%20has%20already%20been%20taken/singular: 1d8b40332a031b5b77a3658e48dd51ca
|
||||||
|
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%20workspace%20username%20has%20already%20been%20taken/singular: b7eadb89c615874f416d9658d0428c4c
|
||||||
|
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%20imports/singular: 6827eca403faa8f89891d17827e72af9
|
Trello%20imports/singular: 6827eca403faa8f89891d17827e72af9
|
||||||
|
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%20workspace/singular: c1c949477e121d73ed6683eddf9cd874
|
Unable%20to%20create%20workspace/singular: c1c949477e121d73ed6683eddf9cd874
|
||||||
|
Unable%20to%20delete%20card/singular: 50ff06aa1265fe464f776aba33f1bef9
|
||||||
|
Unable%20to%20delete%20comment/singular: 550198b2c87f06726a843c79c1026ed9
|
||||||
|
Unable%20to%20remove%20member/singular: 39025a0c53818438829603d213baef06
|
||||||
Unable%20to%20send%20feedback/singular: 656c93265d7e2bef1245b17d85f85ae5
|
Unable%20to%20send%20feedback/singular: 656c93265d7e2bef1245b17d85f85ae5
|
||||||
|
Unable%20to%20update%20board%20URL/singular: 080746884059142358b58d9a44ff7d93
|
||||||
Unable%20to%20update%20board%20visibility/singular: a76a21d561b8943e9276e027a1e3f70d
|
Unable%20to%20update%20board%20visibility/singular: a76a21d561b8943e9276e027a1e3f70d
|
||||||
Unable%20to%20update%20card/singular: eca6002e57af8ec97324eae64e3b5731
|
Unable%20to%20update%20card/singular: eca6002e57af8ec97324eae64e3b5731
|
||||||
|
Unable%20to%20update%20comment/singular: 05e798ed6b0f3fee41f3e8832eb699ea
|
||||||
|
Unable%20to%20update%20labels/singular: dca2bdc3dcf74bc9d95e05156039a291
|
||||||
Unable%20to%20update%20list/singular: 14aa802f91b9b4c05236c8c75afb33da
|
Unable%20to%20update%20list/singular: 14aa802f91b9b4c05236c8c75afb33da
|
||||||
|
Unable%20to%20update%20members/singular: 9a851a6b0c75ee16d25cf2ff4b67b255
|
||||||
|
unassigned%20%3C0%3E%7BmemberName%7D%3C%2F0%3E%20from%20the%20card/singular: 105d4eeb32044126b8dde243bad11d56
|
||||||
|
unassigned%20themselves%20from%20the%20card/singular: 27c6f293c562af6a44348d7f00036d30
|
||||||
Unlimited%20activity%20log/singular: 8c993de94cda0deac19ba14ecafce6a5
|
Unlimited%20activity%20log/singular: 8c993de94cda0deac19ba14ecafce6a5
|
||||||
Unlimited%20boards/singular: cb4fd858137ce825c2f1904d142d8042
|
Unlimited%20boards/singular: cb4fd858137ce825c2f1904d142d8042
|
||||||
Unlimited%20boards%2C%20unlimited%20lists%2C%20unlimited%20cards.%20No%20credit%20card%20required./singular: 8d9733137deb26664d76b55ad561c42c
|
Unlimited%20boards%2C%20unlimited%20lists%2C%20unlimited%20cards.%20No%20credit%20card%20required./singular: 8d9733137deb26664d76b55ad561c42c
|
||||||
Unlimited%20cards/singular: f44c1050c4d6aa39c8cdf5f4a7d12577
|
Unlimited%20cards/singular: f44c1050c4d6aa39c8cdf5f4a7d12577
|
||||||
Unlimited%20comments/singular: 31dbcf487c49a9997142e96bb3ab0040
|
Unlimited%20comments/singular: 31dbcf487c49a9997142e96bb3ab0040
|
||||||
Unlimited%20lists/singular: 5c418701b27b9acbc5dd199719007362
|
Unlimited%20lists/singular: 5c418701b27b9acbc5dd199719007362
|
||||||
|
Update/singular: 079fc039262fd31b10532929685c2d1b
|
||||||
Update%20label/singular: 97880b503dfe956941c5f23025a1c102
|
Update%20label/singular: 97880b503dfe956941c5f23025a1c102
|
||||||
|
updated%20the%20description/singular: 70fd604b6dc957a75be90ea77572d76d
|
||||||
|
updated%20the%20title/singular: 98cf8f12923ec1a58e767e3a40863b21
|
||||||
|
updated%20the%20title%20to%20%3C0%3E%7BtoTitle%7D%3C%2F0%3E/singular: 70cdf681ec2b6ef8e14397bc1f93f945
|
||||||
|
Upgrade/singular: 63c3b52882e0d779859307d672c178c2
|
||||||
|
URL%20can%20only%20contain%20letters%2C%20numbers%2C%20and%20hyphens/singular: 875d37d2d920f98ef60a314eda8a5d9a
|
||||||
|
URL%20cannot%20exceed%2024%20characters/singular: 395c25732b84ac1f5b63ee310da08e93
|
||||||
|
URL%20must%20be%20at%20least%203%20characters%20long/singular: 1b74ac9ec61fd34b16530ed599009ed1
|
||||||
|
User/singular: 61073457a5c3901084b557d065f876be
|
||||||
|
View%20and%20manage%20your%20API%20keys./singular: 88e23ee79056846dd9ce63ceee690549
|
||||||
|
View%20and%20manage%20your%20billing%20and%20subscription./singular: 7469502543aac6f02149813546d1cb38
|
||||||
View%20docs/singular: 43ce54f27bb1e3f1880b56e03ab38871
|
View%20docs/singular: 43ce54f27bb1e3f1880b56e03ab38871
|
||||||
View%20our%20roadmap./singular: 1dd7ec80e1635951b1c90d592d1d07dc
|
View%20our%20roadmap./singular: 1dd7ec80e1635951b1c90d592d1d07dc
|
||||||
Visibility/singular: 1ff412f8fcde0f460acbf11153649646
|
Visibility/singular: 1ff412f8fcde0f460acbf11153649646
|
||||||
We%20are%20using%20the%20%3C0%3EAGPL-3.0%20license%3C%2F0%3E./singular: 08c5ddd938a2a70f61d684e41e18697a
|
We%20are%20using%20the%20%3C0%3EAGPL-3.0%20license%3C%2F0%3E./singular: 08c5ddd938a2a70f61d684e41e18697a
|
||||||
We're%20just%20getting%20started.%20/singular: 2dfa121e0ff1914e08068308a81973c0
|
We're%20just%20getting%20started.%20/singular: 2dfa121e0ff1914e08068308a81973c0
|
||||||
|
Welcome%20back/singular: 4928884739ba559e6e4b960e80fe1452
|
||||||
What%20license%20are%20you%20using%3F/singular: 59d00e1bf28edc21b77fec94875f57ca
|
What%20license%20are%20you%20using%3F/singular: 59d00e1bf28edc21b77fec94875f57ca
|
||||||
What's%20the%20difference%20between%20Kan%20and%20Trello%3F/singular: 7a02f507dd6abbfab1fdae0b172d31b7
|
What's%20the%20difference%20between%20Kan%20and%20Trello%3F/singular: 7a02f507dd6abbfab1fdae0b172d31b7
|
||||||
When%20Trello%20launched%20in%202011%2C%20it%20blew%20everyone%20away%20with%20its%20carefully%20designed%20simplicity%2C%20but%20over%20time%20(and%20atlassification)%20it%20lost%20its%20magic%20and%20grew%20into%20something%20closer%20to%20%22Jira%20Lite%22.%20This%20project%20is%20our%20attempt%20to%20recapture%20the%20original%20magic%20of%20Trello's%20simplicity%2C%20in%20open%20source./singular: 60a55e9bd16c3d1beff1a6c7633c5911
|
When%20Trello%20launched%20in%202011%2C%20it%20blew%20everyone%20away%20with%20its%20carefully%20designed%20simplicity%2C%20but%20over%20time%20(and%20atlassification)%20it%20lost%20its%20magic%20and%20grew%20into%20something%20closer%20to%20%22Jira%20Lite%22.%20This%20project%20is%20our%20attempt%20to%20recapture%20the%20original%20magic%20of%20Trello's%20simplicity%2C%20in%20open%20source./singular: 60a55e9bd16c3d1beff1a6c7633c5911
|
||||||
Why%20make%20an%20open%20source%20Trello%3F/singular: 090833338ba8c64ef7a75e0f2615b34b
|
Why%20make%20an%20open%20source%20Trello%3F/singular: 090833338ba8c64ef7a75e0f2615b34b
|
||||||
Workspace/singular: b63ef0e99ee6f7fef6cbe4971ca6cf0f
|
Workspace/singular: b63ef0e99ee6f7fef6cbe4971ca6cf0f
|
||||||
|
Workspace%20deleted/singular: bc5c267ce3f04de94ff1379cf2ca48a3
|
||||||
|
Workspace%20description/singular: 7159d620b7ef5c5afef734989a5be08b
|
||||||
|
Workspace%20description%20cannot%20exceed%20280%20characters/singular: d0e83b9a0304d7c36cf8c85d98783ce2
|
||||||
|
Workspace%20description%20must%20be%20at%20least%203%20characters%20long/singular: 5ebac407715e6569adc381ebbb4f32d4
|
||||||
|
Workspace%20description%20updated/singular: 19ddb6ee9ead26982a90dee426f3ee1f
|
||||||
Workspace%20members/singular: 1857292f9567c556c781a23bd7e10544
|
Workspace%20members/singular: 1857292f9567c556c781a23bd7e10544
|
||||||
Workspace%20name/singular: 855614bbf862fd6a9f6a3e76943f92d4
|
Workspace%20name/singular: 855614bbf862fd6a9f6a3e76943f92d4
|
||||||
|
Workspace%20name%20cannot%20exceed%2024%20characters/singular: c8b8f79fb83abf8329cc34c1f51fcfc9
|
||||||
|
Workspace%20name%20must%20be%20at%20least%203%20characters%20long/singular: e448ea97418d44b18b4c21c22b8ba779
|
||||||
|
Workspace%20name%20updated/singular: 3206ea410ee1ea4182b27ac0d89f92a1
|
||||||
|
Workspace%20slug%20updated/singular: 527b92711d38cb35b40741df43aef047
|
||||||
|
Workspace%20URL/singular: f4397a838da0f3a44cbd3ebe408ed6c3
|
||||||
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
|
||||||
You%20can%20invite%20team%20members%20by%20clicking%20the%20%22Invite%22%20button%20in%20the%20top%20right%20corner%20of%20the%20%3C0%3Emembers%20page%3C%2F0%3E%20and%20entering%20their%20email%20address.%20They%20will%20receive%20an%20email%20with%20a%20link%20to%20join%20the%20workspace./singular: 47e125eb9b4c11cab5a2f4b3ab08e883
|
You%20can%20invite%20team%20members%20by%20clicking%20the%20%22Invite%22%20button%20in%20the%20top%20right%20corner%20of%20the%20%3C0%3Emembers%20page%3C%2F0%3E%20and%20entering%20their%20email%20address.%20They%20will%20receive%20an%20email%20with%20a%20link%20to%20join%20the%20workspace./singular: 47e125eb9b4c11cab5a2f4b3ab08e883
|
||||||
You%20can%20self-host%20by%20following%20the%20instructions%20in%20our%20%3C0%3Erepo%3C%2F0%3E./singular: a6152a41b2d8f64d5a657e5b8bc808a9
|
You%20can%20self-host%20by%20following%20the%20instructions%20in%20our%20%3C0%3Erepo%3C%2F0%3E./singular: a6152a41b2d8f64d5a657e5b8bc808a9
|
||||||
|
You%20have%20been%20logged%20in%20successfully./singular: ef8fad1dce13ae4112f17c5258655fea
|
||||||
|
You%20have%20been%20signed%20up%20successfully./singular: f614a6e3b45f5ffb9a3b0fb420fef84b
|
||||||
|
Your%20account%20has%20been%20deleted./singular: 8c8d944e07388c5877effdb2c2803dcf
|
||||||
Your%20boards%20have%20been%20imported./singular: 403972e7a25afc2415762c1c2b1ec868
|
Your%20boards%20have%20been%20imported./singular: 403972e7a25afc2415762c1c2b1ec868
|
||||||
|
Your%20display%20name%20has%20been%20updated./singular: 15e5fff36c554c16ec5214427fae1bf4
|
||||||
|
Your%20profile%20image%20has%20been%20updated./singular: 099e1780348cd7bf617721344371a430
|
||||||
|
Your%20Trello%20account%20has%20been%20disconnected./singular: 126aeeb73f7e4cad338056e4c933f39b
|
||||||
|
Your%20Trello%20account%20is%20connected./singular: 0499d193001fce2fbec445ae034ce512
|
||||||
|
Your%20workspace%20description%20has%20been%20updated./singular: 98de6f65c567c6654cc0adce3c9d5656
|
||||||
|
Your%20workspace%20has%20been%20deleted./singular: e7a3efcfc7dd18cb3e917acb67498292
|
||||||
|
Your%20workspace%20name%20has%20been%20updated./singular: a87ea3b0d71e6dc5dd525d77114a9322
|
||||||
|
Your%20workspace%20slug%20has%20been%20updated./singular: c808949b9b2b4a9aba2472f5d1050167
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { SocialProvider } from "better-auth/social-providers";
|
import type { SocialProvider } from "better-auth/social-providers";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { env } from "next-runtime-env";
|
import { env } from "next-runtime-env";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -183,8 +185,8 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
|||||||
{
|
{
|
||||||
onSuccess: () =>
|
onSuccess: () =>
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Success",
|
header: t`Success`,
|
||||||
message: "You have been signed up successfully.",
|
message: t`You have been signed up successfully.`,
|
||||||
icon: "success",
|
icon: "success",
|
||||||
}),
|
}),
|
||||||
onError: ({ error }) => setLoginError(error.message),
|
onError: ({ error }) => setLoginError(error.message),
|
||||||
@@ -200,8 +202,8 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
|||||||
{
|
{
|
||||||
onSuccess: () =>
|
onSuccess: () =>
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Success",
|
header: t`Success`,
|
||||||
message: "You have been logged in successfully.",
|
message: t`You have been logged in successfully.`,
|
||||||
icon: "success",
|
icon: "success",
|
||||||
}),
|
}),
|
||||||
onError: ({ error }) => setLoginError(error.message),
|
onError: ({ error }) => setLoginError(error.message),
|
||||||
@@ -236,7 +238,7 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
setLoginError(
|
setLoginError(
|
||||||
`Failed to login with ${provider.at(0)?.toUpperCase() + provider.slice(1)}. Please try again.`,
|
t`Failed to login with ${provider.at(0)?.toUpperCase() + provider.slice(1)}. Please try again.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -263,7 +265,7 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
|||||||
fullWidth
|
fullWidth
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
Continue with {provider.name}
|
<Trans>Continue with {provider.name}</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -274,7 +276,7 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
|||||||
<div className="mb-[1.5rem] flex w-full items-center gap-4">
|
<div className="mb-[1.5rem] flex w-full items-center gap-4">
|
||||||
<div className="h-[1px] w-full bg-light-600 dark:bg-dark-600" />
|
<div className="h-[1px] w-full bg-light-600 dark:bg-dark-600" />
|
||||||
<span className="text-sm text-light-900 dark:text-dark-900">
|
<span className="text-sm text-light-900 dark:text-dark-900">
|
||||||
or
|
{t`or`}
|
||||||
</span>
|
</span>
|
||||||
<div className="h-[1px] w-full bg-light-600 dark:bg-dark-600" />
|
<div className="h-[1px] w-full bg-light-600 dark:bg-dark-600" />
|
||||||
</div>
|
</div>
|
||||||
@@ -284,11 +286,11 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
|||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
{...register("name", { required: true })}
|
{...register("name", { required: true })}
|
||||||
placeholder="Enter your name"
|
placeholder={t`Enter your name`}
|
||||||
/>
|
/>
|
||||||
{errors.name && (
|
{errors.name && (
|
||||||
<p className="mt-2 text-xs text-red-400">
|
<p className="mt-2 text-xs text-red-400">
|
||||||
Please enter a valid name
|
{t`Please enter a valid name`}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -296,11 +298,11 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
|||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
{...register("email", { required: true })}
|
{...register("email", { required: true })}
|
||||||
placeholder="Enter your email address"
|
placeholder={t`Enter your email address`}
|
||||||
/>
|
/>
|
||||||
{errors.email && (
|
{errors.email && (
|
||||||
<p className="mt-2 text-xs text-red-400">
|
<p className="mt-2 text-xs text-red-400">
|
||||||
Please enter a valid email address
|
{t`Please enter a valid email address`}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -309,11 +311,11 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
|||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
{...register("password", { required: true })}
|
{...register("password", { required: true })}
|
||||||
placeholder="Enter your password"
|
placeholder={t`Enter your password`}
|
||||||
/>
|
/>
|
||||||
{errors.password && (
|
{errors.password && (
|
||||||
<p className="mt-2 text-xs text-red-400">
|
<p className="mt-2 text-xs text-red-400">
|
||||||
Please enter a valid password
|
{t`Please enter a valid password`}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -329,10 +331,10 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
|||||||
size="lg"
|
size="lg"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
>
|
>
|
||||||
{isSignUp ? "Sign up with " : "Continue with "}
|
{isSignUp ? t`Sign up with ` : t`Continue with `}
|
||||||
{!isCredentialsEnabled || (password && password.length !== 0)
|
{!isCredentialsEnabled || (password && password.length !== 0)
|
||||||
? "email"
|
? t`email`
|
||||||
: "magic link"}
|
: t`magic link`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
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
@@ -1,5 +1,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { env } from "next-runtime-env";
|
import { env } from "next-runtime-env";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title="Login | kan.bn" />
|
<PageHead title={t`Login | kan.bn`} />
|
||||||
<main className="h-screen bg-light-100 pt-20 dark:bg-dark-50 sm:pt-0">
|
<main className="h-screen bg-light-100 pt-20 dark:bg-dark-50 sm:pt-0">
|
||||||
<div className="justify-top flex h-full flex-col items-center px-4 sm:justify-center">
|
<div className="justify-top flex h-full flex-col items-center px-4 sm:justify-center">
|
||||||
<div className="z-10 flex w-full flex-col items-center">
|
<div className="z-10 flex w-full flex-col items-center">
|
||||||
@@ -36,12 +38,15 @@ export default function LoginPage() {
|
|||||||
</h1>
|
</h1>
|
||||||
</Link>
|
</Link>
|
||||||
<p className="mb-10 text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
|
<p className="mb-10 text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
|
||||||
{isMagicLinkSent ? "Check your inbox" : "Welcome back"}
|
{isMagicLinkSent ? t`Check your inbox` : t`Welcome back`}
|
||||||
</p>
|
</p>
|
||||||
{isMagicLinkSent ? (
|
{isMagicLinkSent ? (
|
||||||
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
|
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
<p className="text-md mt-2 text-center text-light-1000 dark:text-dark-1000">
|
<p className="text-md mt-2 text-center text-light-1000 dark:text-dark-1000">
|
||||||
{`Click on the link we've sent to ${magicLinkRecipient} to sign in.`}
|
<Trans>
|
||||||
|
Click on the link we've sent to {magicLinkRecipient} to sign
|
||||||
|
in.
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -53,10 +58,12 @@ export default function LoginPage() {
|
|||||||
)}
|
)}
|
||||||
{!isSignUpDisabled && (
|
{!isSignUpDisabled && (
|
||||||
<p className="mt-4 text-sm text-light-1000 dark:text-dark-1000">
|
<p className="mt-4 text-sm text-light-1000 dark:text-dark-1000">
|
||||||
Don't have an account?{" "}
|
<Trans>
|
||||||
<span className="underline">
|
Don't have an account?{" "}
|
||||||
<Link href="/signup">Sign up</Link>
|
<span className="underline">
|
||||||
</span>
|
<Link href="/signup">Sign up</Link>
|
||||||
|
</span>
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
|
import { Trans } from "@lingui/react/macro";
|
||||||
|
import { env } from "next-runtime-env";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { authClient } from "@kan/auth/client";
|
import { authClient } from "@kan/auth/client";
|
||||||
@@ -7,34 +10,51 @@ import { authClient } from "@kan/auth/client";
|
|||||||
import { Auth } from "~/components/AuthForm";
|
import { Auth } from "~/components/AuthForm";
|
||||||
import { PageHead } from "~/components/PageHead";
|
import { PageHead } from "~/components/PageHead";
|
||||||
import PatternedBackground from "~/components/PatternedBackground";
|
import PatternedBackground from "~/components/PatternedBackground";
|
||||||
import { env } from "next-runtime-env";
|
|
||||||
|
|
||||||
export default function SignupPage() {
|
export default function SignUpPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const isSignUpDisabled = env("NEXT_PUBLIC_DISABLE_SIGN_UP") === "true";
|
||||||
const [isMagicLinkSent, setIsMagicLinkSent] = useState<boolean>(false);
|
const [isMagicLinkSent, setIsMagicLinkSent] = useState<boolean>(false);
|
||||||
const [magicLinkRecipient, setMagicLinkRecipient] = useState<string>("");
|
const [magicLinkRecipient, setMagicLinkRecipient] = useState<string>("");
|
||||||
|
|
||||||
|
const { data } = authClient.useSession();
|
||||||
|
|
||||||
|
if (data?.user.id) router.push("/boards");
|
||||||
|
|
||||||
const handleMagicLinkSent = (value: boolean, recipient: string) => {
|
const handleMagicLinkSent = (value: boolean, recipient: string) => {
|
||||||
setIsMagicLinkSent(value);
|
setIsMagicLinkSent(value);
|
||||||
setMagicLinkRecipient(recipient);
|
setMagicLinkRecipient(recipient);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data } = authClient.useSession();
|
if (isSignUpDisabled) {
|
||||||
|
|
||||||
if (data?.user.id) router.push("/boards");
|
|
||||||
|
|
||||||
if (env("NEXT_PUBLIC_DISABLE_SIGN_UP")?.toLowerCase() === "true") {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-light-100 dark:bg-dark-50 h-screen items-center justify-center">
|
<>
|
||||||
<p className="mb-2 text-light-1000 dark:text-dark-1000 font-semibold text-sm">Sign up is disabled</p>
|
<PageHead title={t`Sign up | kan.bn`} />
|
||||||
<Link href="/login" className="text-light-1000 dark:text-dark-1000 font-semibold text-sm rounded-md border border-light-900 dark:border-dark-900 px-4 py-2">Login</Link>
|
<main className="h-screen bg-light-100 pt-20 dark:bg-dark-50 sm:pt-0">
|
||||||
</div>
|
<div className="justify-top flex h-full flex-col items-center px-4 sm:justify-center">
|
||||||
)
|
<div className="z-10 flex w-full flex-col items-center">
|
||||||
|
<Link href="/">
|
||||||
|
<h1 className="mb-6 text-lg font-bold tracking-tight text-light-1000 dark:text-dark-1000">
|
||||||
|
kan.bn
|
||||||
|
</h1>
|
||||||
|
</Link>
|
||||||
|
<p className="mb-10 text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
|
||||||
|
{t`Sign up disabled`}
|
||||||
|
</p>
|
||||||
|
<p className="text-md text-center text-light-1000 dark:text-dark-1000">
|
||||||
|
{t`Sign up is currently disabled. Please try again later.`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<PatternedBackground />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title="Sign up | kan.bn" />
|
<PageHead title={t`Sign up | kan.bn`} />
|
||||||
<main className="h-screen bg-light-100 pt-20 dark:bg-dark-50 sm:pt-0">
|
<main className="h-screen bg-light-100 pt-20 dark:bg-dark-50 sm:pt-0">
|
||||||
<div className="justify-top flex h-full flex-col items-center px-4 sm:justify-center">
|
<div className="justify-top flex h-full flex-col items-center px-4 sm:justify-center">
|
||||||
<div className="z-10 flex w-full flex-col items-center">
|
<div className="z-10 flex w-full flex-col items-center">
|
||||||
@@ -44,12 +64,15 @@ export default function SignupPage() {
|
|||||||
</h1>
|
</h1>
|
||||||
</Link>
|
</Link>
|
||||||
<p className="mb-10 text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
|
<p className="mb-10 text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
|
||||||
{isMagicLinkSent ? "Check your inbox" : "Create your account"}
|
{isMagicLinkSent ? t`Check your inbox` : t`Get started`}
|
||||||
</p>
|
</p>
|
||||||
{isMagicLinkSent ? (
|
{isMagicLinkSent ? (
|
||||||
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
|
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
<p className="text-md mt-2 text-center text-light-1000 dark:text-dark-1000">
|
<p className="text-md mt-2 text-center text-light-1000 dark:text-dark-1000">
|
||||||
{`Click on the link we've sent to ${magicLinkRecipient} to sign in.`}
|
<Trans>
|
||||||
|
Click on the link we've sent to {magicLinkRecipient} to sign
|
||||||
|
in.
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -60,10 +83,12 @@ export default function SignupPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<p className="mt-4 text-sm text-light-1000 dark:text-dark-1000">
|
<p className="mt-4 text-sm text-light-1000 dark:text-dark-1000">
|
||||||
Already have an account?{" "}
|
<Trans>
|
||||||
<span className="underline">
|
Already have an account?{" "}
|
||||||
<Link href="/login">Sign in</Link>
|
<span className="underline">
|
||||||
</span>
|
<Link href="/login">Sign in</Link>
|
||||||
|
</span>
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<PatternedBackground />
|
<PatternedBackground />
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import { HiEllipsisHorizontal, HiLink, HiOutlineTrash } from "react-icons/hi2";
|
import { HiEllipsisHorizontal, HiLink, HiOutlineTrash } from "react-icons/hi2";
|
||||||
|
|
||||||
import Dropdown from "~/components/Dropdown";
|
import Dropdown from "~/components/Dropdown";
|
||||||
@@ -11,12 +12,12 @@ export default function BoardDropdown({ isLoading }: { isLoading: boolean }) {
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
label: "Edit board URL",
|
label: t`Edit board URL`,
|
||||||
action: () => openModal("UPDATE_BOARD_SLUG"),
|
action: () => openModal("UPDATE_BOARD_SLUG"),
|
||||||
icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />,
|
icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Delete board",
|
label: t`Delete board`,
|
||||||
action: () => openModal("DELETE_BOARD"),
|
action: () => openModal("DELETE_BOARD"),
|
||||||
icon: <HiOutlineTrash className="h-[16px] w-[16px] text-dark-900" />,
|
icon: <HiOutlineTrash className="h-[16px] w-[16px] text-dark-900" />,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import {
|
import {
|
||||||
HiMiniXMark,
|
HiMiniXMark,
|
||||||
HiOutlineTag,
|
HiOutlineTag,
|
||||||
@@ -89,7 +90,7 @@ const Filters = ({
|
|||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
key: "members",
|
key: "members",
|
||||||
label: "Members",
|
label: t`Members`,
|
||||||
icon: <HiOutlineUserCircle size={16} />,
|
icon: <HiOutlineUserCircle size={16} />,
|
||||||
items: formattedMembers,
|
items: formattedMembers,
|
||||||
},
|
},
|
||||||
@@ -97,7 +98,7 @@ const Filters = ({
|
|||||||
: []),
|
: []),
|
||||||
{
|
{
|
||||||
key: "labels",
|
key: "labels",
|
||||||
label: "Labels",
|
label: t`Labels`,
|
||||||
icon: <HiOutlineTag size={16} />,
|
icon: <HiOutlineTag size={16} />,
|
||||||
items: formattedLabels,
|
items: formattedLabels,
|
||||||
},
|
},
|
||||||
@@ -145,13 +146,13 @@ const Filters = ({
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
iconLeft={<IoFilterOutline />}
|
iconLeft={<IoFilterOutline />}
|
||||||
>
|
>
|
||||||
Filter
|
{t`Filter`}
|
||||||
</Button>
|
</Button>
|
||||||
{numOfFilters > 0 && (
|
{numOfFilters > 0 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={clearFilters}
|
onClick={clearFilters}
|
||||||
aria-label="Clear filters"
|
aria-label={t`Clear filters`}
|
||||||
className="group absolute -right-[8px] -top-[8px] flex h-5 w-5 items-center justify-center rounded-full border-2 border-light-100 bg-light-1000 text-[8px] font-[700] text-light-600 dark:border-dark-50 dark:bg-dark-1000 dark:text-dark-600"
|
className="group absolute -right-[8px] -top-[8px] flex h-5 w-5 items-center justify-center rounded-full border-2 border-light-100 bg-light-1000 text-[8px] font-[700] text-light-600 dark:border-dark-50 dark:bg-dark-1000 dark:text-dark-600"
|
||||||
>
|
>
|
||||||
<span className="group-hover:hidden">{numOfFilters}</span>
|
<span className="group-hover:hidden">{numOfFilters}</span>
|
||||||
|
|||||||
@@ -92,9 +92,14 @@ export function NewCardForm({
|
|||||||
args.labelPublicIds.includes(label.publicId),
|
args.labelPublicIds.includes(label.publicId),
|
||||||
),
|
),
|
||||||
members:
|
members:
|
||||||
oldBoard.workspace.members.filter((member) =>
|
oldBoard.workspace.members
|
||||||
args.memberPublicIds.includes(member.publicId),
|
.filter((member) =>
|
||||||
) ?? [],
|
args.memberPublicIds.includes(member.publicId),
|
||||||
|
)
|
||||||
|
.map((member) => ({
|
||||||
|
...member,
|
||||||
|
deletedAt: null,
|
||||||
|
})) ?? [],
|
||||||
_filteredLabels: labelPublicIds.map((id) => ({ publicId: id })),
|
_filteredLabels: labelPublicIds.map((id) => ({ publicId: id })),
|
||||||
_filteredMembers: memberPublicIds.map((id) => ({ publicId: id })),
|
_filteredMembers: memberPublicIds.map((id) => ({ publicId: id })),
|
||||||
index: position === "start" ? 0 : list.cards.length,
|
index: position === "start" ? 0 : list.cards.length,
|
||||||
@@ -168,6 +173,7 @@ export function NewCardForm({
|
|||||||
member.user?.name ?? null,
|
member.user?.name ?? null,
|
||||||
member.user?.email ?? member.email,
|
member.user?.email ?? member.email,
|
||||||
),
|
),
|
||||||
|
selected: memberPublicIds.includes(member.publicId),
|
||||||
leftIcon: (
|
leftIcon: (
|
||||||
<Avatar
|
<Avatar
|
||||||
size="xs"
|
size="xs"
|
||||||
@@ -281,7 +287,7 @@ export function NewCardForm({
|
|||||||
>
|
>
|
||||||
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-600 bg-light-200 px-2 py-1 text-left text-xs text-light-800 hover:bg-light-300 dark:border-dark-600 dark:bg-dark-400 dark:text-dark-1000 dark:hover:bg-dark-500">
|
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-600 bg-light-200 px-2 py-1 text-left text-xs text-light-800 hover:bg-light-300 dark:border-dark-600 dark:bg-dark-400 dark:text-dark-1000 dark:hover:bg-dark-500">
|
||||||
{!memberPublicIds.length ? (
|
{!memberPublicIds.length ? (
|
||||||
"Members"
|
t`Members`
|
||||||
) : (
|
) : (
|
||||||
<div className="flex -space-x-1 overflow-hidden">
|
<div className="flex -space-x-1 overflow-hidden">
|
||||||
{memberPublicIds.map((memberPublicId) => {
|
{memberPublicIds.map((memberPublicId) => {
|
||||||
@@ -322,7 +328,7 @@ export function NewCardForm({
|
|||||||
>
|
>
|
||||||
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-600 bg-light-200 px-2 py-1 text-left text-xs text-light-800 hover:bg-light-300 dark:border-dark-600 dark:bg-dark-400 dark:text-dark-1000 dark:hover:bg-dark-500">
|
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-600 bg-light-200 px-2 py-1 text-left text-xs text-light-800 hover:bg-light-300 dark:border-dark-600 dark:bg-dark-400 dark:text-dark-1000 dark:hover:bg-dark-500">
|
||||||
{!labelPublicIds.length ? (
|
{!labelPublicIds.length ? (
|
||||||
"Labels"
|
t`Labels`
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import { useEffect } 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";
|
||||||
@@ -73,8 +74,8 @@ export function NewListForm({
|
|||||||
onError: (_error, _newList, context) => {
|
onError: (_error, _newList, context) => {
|
||||||
utils.board.byId.setData(queryParams, context?.previousState);
|
utils.board.byId.setData(queryParams, context?.previousState);
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Unable to create list",
|
header: t`Unable to create list`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -108,7 +109,7 @@ export function NewListForm({
|
|||||||
<div className="px-5 pt-5">
|
<div className="px-5 pt-5">
|
||||||
<div className="flex w-full items-center justify-between pb-4">
|
<div className="flex w-full items-center justify-between pb-4">
|
||||||
<h2 className="text-sm font-bold text-neutral-900 dark:text-dark-1000">
|
<h2 className="text-sm font-bold text-neutral-900 dark:text-dark-1000">
|
||||||
New list
|
{t`New list`}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -124,7 +125,7 @@ export function NewListForm({
|
|||||||
|
|
||||||
<Input
|
<Input
|
||||||
id="list-name"
|
id="list-name"
|
||||||
placeholder="List name"
|
placeholder={t`List name`}
|
||||||
{...register("name")}
|
{...register("name")}
|
||||||
onKeyDown={async (e) => {
|
onKeyDown={async (e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
@@ -136,7 +137,7 @@ export function NewListForm({
|
|||||||
</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">
|
||||||
<Toggle
|
<Toggle
|
||||||
label="Create another"
|
label={t`Create another`}
|
||||||
isChecked={isCreateAnotherEnabled}
|
isChecked={isCreateAnotherEnabled}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
setValue("isCreateAnotherEnabled", !isCreateAnotherEnabled)
|
setValue("isCreateAnotherEnabled", !isCreateAnotherEnabled)
|
||||||
@@ -144,7 +145,7 @@ export function NewListForm({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button type="submit">Create list</Button>
|
<Button type="submit">{t`Create list`}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import { env } from "next-runtime-env";
|
import { env } from "next-runtime-env";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -12,20 +13,6 @@ import { useModal } from "~/providers/modal";
|
|||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
slug: z
|
|
||||||
.string()
|
|
||||||
.min(3, {
|
|
||||||
message: "Board URL must be at least 3 characters long",
|
|
||||||
})
|
|
||||||
.max(60, { message: "Board URL cannot exceed 60 characters" })
|
|
||||||
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/, {
|
|
||||||
message: "Board URL can only contain letters, numbers, and hyphens",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
type FormValues = z.infer<typeof schema>;
|
|
||||||
|
|
||||||
interface QueryParams {
|
interface QueryParams {
|
||||||
boardPublicId: string;
|
boardPublicId: string;
|
||||||
members: string[];
|
members: string[];
|
||||||
@@ -47,6 +34,20 @@ export function UpdateBoardSlugForm({
|
|||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
slug: z
|
||||||
|
.string()
|
||||||
|
.min(3, {
|
||||||
|
message: t`Board URL must be at least 3 characters long`,
|
||||||
|
})
|
||||||
|
.max(60, { message: t`Board URL cannot exceed 60 characters` })
|
||||||
|
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/, {
|
||||||
|
message: t`Board URL can only contain letters, numbers, and hyphens`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormValues = z.infer<typeof schema>;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -67,8 +68,8 @@ export function UpdateBoardSlugForm({
|
|||||||
const updateBoardSlug = api.board.update.useMutation({
|
const updateBoardSlug = api.board.update.useMutation({
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Unable to update board URL",
|
header: t`Unable to update board URL`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -78,17 +79,15 @@ export function UpdateBoardSlugForm({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkBoardSlugAvailability =
|
const checkBoardSlugAvailability = api.board.checkSlugAvailability.useQuery(
|
||||||
api.board.checkSlugAvailability.useQuery(
|
{
|
||||||
{
|
boardSlug: debouncedSlug,
|
||||||
boardSlug: debouncedSlug,
|
boardPublicId,
|
||||||
boardPublicId,
|
},
|
||||||
},
|
{
|
||||||
{
|
enabled: !!debouncedSlug && debouncedSlug !== boardSlug && !errors.slug,
|
||||||
enabled:
|
},
|
||||||
!!debouncedSlug && debouncedSlug !== boardSlug && !errors.slug,
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const isBoardSlugAvailable = checkBoardSlugAvailability.data;
|
const isBoardSlugAvailable = checkBoardSlugAvailability.data;
|
||||||
|
|
||||||
@@ -100,7 +99,7 @@ export function UpdateBoardSlugForm({
|
|||||||
|
|
||||||
const onSubmit = (data: FormValues) => {
|
const onSubmit = (data: FormValues) => {
|
||||||
if (!isBoardSlugAvailable) return;
|
if (!isBoardSlugAvailable) return;
|
||||||
if (isBoardSlugAvailable?.isReserved) return;
|
if (isBoardSlugAvailable.isReserved) return;
|
||||||
|
|
||||||
updateBoardSlug.mutate({
|
updateBoardSlug.mutate({
|
||||||
slug: data.slug,
|
slug: data.slug,
|
||||||
@@ -113,7 +112,7 @@ export function UpdateBoardSlugForm({
|
|||||||
<div className="px-5 pt-5">
|
<div className="px-5 pt-5">
|
||||||
<div className="flex w-full items-center justify-between pb-4">
|
<div className="flex w-full items-center justify-between pb-4">
|
||||||
<h2 className="text-sm font-bold text-neutral-900 dark:text-dark-1000">
|
<h2 className="text-sm font-bold text-neutral-900 dark:text-dark-1000">
|
||||||
Edit board URL
|
{t`Edit board URL`}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -130,9 +129,12 @@ export function UpdateBoardSlugForm({
|
|||||||
<Input
|
<Input
|
||||||
id="board-slug"
|
id="board-slug"
|
||||||
{...register("slug")}
|
{...register("slug")}
|
||||||
errorMessage={errors.slug?.message || (isBoardSlugAvailable?.isReserved
|
errorMessage={
|
||||||
? "This board URL has already been taken"
|
errors.slug?.message ||
|
||||||
: undefined)}
|
(isBoardSlugAvailable?.isReserved
|
||||||
|
? t`This board URL has already been taken`
|
||||||
|
: undefined)
|
||||||
|
}
|
||||||
prefix={`${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") {
|
||||||
@@ -161,7 +163,7 @@ export function UpdateBoardSlugForm({
|
|||||||
isBoardSlugAvailable?.isReserved
|
isBoardSlugAvailable?.isReserved
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Update
|
{t`Update`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import { t } from "@lingui/core/macro";
|
||||||
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
|
import { de, enGB, es, fr, it, nl } from "date-fns/locale";
|
||||||
import {
|
import {
|
||||||
HiOutlineArrowLeft,
|
HiOutlineArrowLeft,
|
||||||
HiOutlineArrowRight,
|
HiOutlineArrowRight,
|
||||||
@@ -8,24 +11,24 @@ import {
|
|||||||
HiOutlineUserMinus,
|
HiOutlineUserMinus,
|
||||||
HiOutlineUserPlus,
|
HiOutlineUserPlus,
|
||||||
} from "react-icons/hi2";
|
} from "react-icons/hi2";
|
||||||
|
|
||||||
import type { GetCardByIdOutput } from "@kan/api/types";
|
import type { GetCardByIdOutput } from "@kan/api/types";
|
||||||
|
import { authClient } from "@kan/auth/client";
|
||||||
|
|
||||||
import Avatar from "~/components/Avatar";
|
import Avatar from "~/components/Avatar";
|
||||||
|
import { useLocalisation } from "~/hooks/useLocalisation";
|
||||||
import Comment from "./Comment";
|
import Comment from "./Comment";
|
||||||
import { authClient } from "@kan/auth/client";
|
|
||||||
|
|
||||||
type ActivityType =
|
type ActivityType =
|
||||||
NonNullable<GetCardByIdOutput>["activities"][number]["type"];
|
NonNullable<GetCardByIdOutput>["activities"][number]["type"];
|
||||||
|
|
||||||
const ACTIVITY_TYPE_MAP = {
|
const dateLocaleMap = {
|
||||||
"card.created": "created the card",
|
en: enGB,
|
||||||
"card.updated.title": "updated the title",
|
fr: fr,
|
||||||
"card.updated.description": "updated the description",
|
de: de,
|
||||||
"card.updated.list": "moved the card to another list",
|
es: es,
|
||||||
"card.updated.label.added": "added a label to the card",
|
it: it,
|
||||||
"card.updated.label.removed": "removed a label from the card",
|
nl: nl,
|
||||||
"card.updated.member.added": "added a member to the card",
|
|
||||||
"card.updated.member.removed": "removed a member from the card",
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const getActivityText = ({
|
const getActivityText = ({
|
||||||
@@ -45,6 +48,17 @@ const getActivityText = ({
|
|||||||
isSelf: boolean;
|
isSelf: boolean;
|
||||||
label: string | null;
|
label: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
|
const ACTIVITY_TYPE_MAP = {
|
||||||
|
"card.created": t`created the card`,
|
||||||
|
"card.updated.title": t`updated the title`,
|
||||||
|
"card.updated.description": t`updated the description`,
|
||||||
|
"card.updated.list": t`moved the card to another list`,
|
||||||
|
"card.updated.label.added": t`added a label to the card`,
|
||||||
|
"card.updated.label.removed": t`removed a label from the card`,
|
||||||
|
"card.updated.member.added": t`added a member to the card`,
|
||||||
|
"card.updated.member.removed": t`removed a member from the card`,
|
||||||
|
} as const;
|
||||||
|
|
||||||
if (!(type in ACTIVITY_TYPE_MAP)) return null;
|
if (!(type in ACTIVITY_TYPE_MAP)) return null;
|
||||||
const baseText = ACTIVITY_TYPE_MAP[type as keyof typeof ACTIVITY_TYPE_MAP];
|
const baseText = ACTIVITY_TYPE_MAP[type as keyof typeof ACTIVITY_TYPE_MAP];
|
||||||
|
|
||||||
@@ -56,54 +70,54 @@ const getActivityText = ({
|
|||||||
|
|
||||||
if (type === "card.updated.title" && toTitle) {
|
if (type === "card.updated.title" && toTitle) {
|
||||||
return (
|
return (
|
||||||
<>
|
<Trans>
|
||||||
updated the title to <TextHighlight>{toTitle}</TextHighlight>
|
updated the title to <TextHighlight>{toTitle}</TextHighlight>
|
||||||
</>
|
</Trans>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "card.updated.list" && fromList && toList) {
|
if (type === "card.updated.list" && fromList && toList) {
|
||||||
return (
|
return (
|
||||||
<>
|
<Trans>
|
||||||
moved the card from <TextHighlight>{fromList}</TextHighlight> to
|
moved the card from <TextHighlight>{fromList}</TextHighlight> to
|
||||||
<TextHighlight>{toList}</TextHighlight>
|
<TextHighlight>{toList}</TextHighlight>
|
||||||
</>
|
</Trans>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "card.updated.member.added" && memberName) {
|
if (type === "card.updated.member.added" && memberName) {
|
||||||
if (isSelf) return <>self-assigned the card</>;
|
if (isSelf) return <Trans>self-assigned the card</Trans>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Trans>
|
||||||
assigned <TextHighlight>{memberName}</TextHighlight> to the card
|
assigned <TextHighlight>{memberName}</TextHighlight> to the card
|
||||||
</>
|
</Trans>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "card.updated.member.removed" && memberName) {
|
if (type === "card.updated.member.removed" && memberName) {
|
||||||
if (isSelf) return <>unassigned themselves from the card</>;
|
if (isSelf) return <Trans>unassigned themselves from the card</Trans>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Trans>
|
||||||
unassigned <TextHighlight>{memberName}</TextHighlight> from the card
|
unassigned <TextHighlight>{memberName}</TextHighlight> from the card
|
||||||
</>
|
</Trans>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "card.updated.label.added" && label) {
|
if (type === "card.updated.label.added" && label) {
|
||||||
return (
|
return (
|
||||||
<>
|
<Trans>
|
||||||
added label <TextHighlight>{label}</TextHighlight>
|
added label <TextHighlight>{label}</TextHighlight>
|
||||||
</>
|
</Trans>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "card.updated.label.removed" && label) {
|
if (type === "card.updated.label.removed" && label) {
|
||||||
return (
|
return (
|
||||||
<>
|
<Trans>
|
||||||
removed label <TextHighlight>{label}</TextHighlight>
|
removed label <TextHighlight>{label}</TextHighlight>
|
||||||
</>
|
</Trans>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +162,9 @@ const ActivityList = ({
|
|||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = authClient.useSession();
|
const { data } = authClient.useSession();
|
||||||
|
const { locale } = useLocalisation();
|
||||||
|
|
||||||
|
const currentDateLocale = dateLocaleMap[locale] || enGB;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-4 pt-4">
|
<div className="flex flex-col space-y-4 pt-4">
|
||||||
@@ -158,7 +175,7 @@ const ActivityList = ({
|
|||||||
fromList: activity.fromList?.name ?? null,
|
fromList: activity.fromList?.name ?? null,
|
||||||
toList: activity.toList?.name ?? null,
|
toList: activity.toList?.name ?? null,
|
||||||
memberName: activity.member?.user?.name ?? null,
|
memberName: activity.member?.user?.name ?? null,
|
||||||
isSelf: activity.member?.user?.id === data?.user?.id,
|
isSelf: activity.member?.user?.id === data?.user.id,
|
||||||
label: activity.label?.name ?? null,
|
label: activity.label?.name ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -171,10 +188,10 @@ const ActivityList = ({
|
|||||||
name={activity.user?.name ?? ""}
|
name={activity.user?.name ?? ""}
|
||||||
email={activity.user?.email ?? ""}
|
email={activity.user?.email ?? ""}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
createdAt={activity.createdAt}
|
createdAt={activity.createdAt.toISOString()}
|
||||||
comment={activity.comment?.comment}
|
comment={activity.comment?.comment}
|
||||||
isEdited={!!activity.comment?.updatedAt}
|
isEdited={!!activity.comment?.updatedAt}
|
||||||
isAuthor={activity.comment?.createdBy === data?.user?.id}
|
isAuthor={activity.comment?.createdBy === data?.user.id}
|
||||||
isAdmin={isAdmin ?? false}
|
isAdmin={isAdmin ?? false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -213,6 +230,7 @@ const ActivityList = ({
|
|||||||
<span className="space-x-1 text-light-900 dark:text-dark-800">
|
<span className="space-x-1 text-light-900 dark:text-dark-800">
|
||||||
{formatDistanceToNow(new Date(activity.createdAt), {
|
{formatDistanceToNow(new Date(activity.createdAt), {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
|
locale: currentDateLocale,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import ContentEditable from "react-contenteditable";
|
import ContentEditable from "react-contenteditable";
|
||||||
@@ -57,8 +58,8 @@ const Comment = ({
|
|||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Unable to update comment",
|
header: t`Unable to update comment`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -76,7 +77,7 @@ const Comment = ({
|
|||||||
...(isAuthor
|
...(isAuthor
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: "Edit comment",
|
label: t`Edit comment`,
|
||||||
action: () => setIsEditing(true),
|
action: () => setIsEditing(true),
|
||||||
icon: <HiPencil className="h-[16px] w-[16px] text-dark-900" />,
|
icon: <HiPencil className="h-[16px] w-[16px] text-dark-900" />,
|
||||||
},
|
},
|
||||||
@@ -85,7 +86,7 @@ const Comment = ({
|
|||||||
...(isAuthor || isAdmin
|
...(isAuthor || isAdmin
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: "Delete comment",
|
label: t`Delete comment`,
|
||||||
action: () => openModal("DELETE_COMMENT", publicId),
|
action: () => openModal("DELETE_COMMENT", publicId),
|
||||||
icon: <HiTrash className="h-[16px] w-[16px] text-dark-900" />,
|
icon: <HiTrash className="h-[16px] w-[16px] text-dark-900" />,
|
||||||
},
|
},
|
||||||
@@ -117,7 +118,7 @@ const Comment = ({
|
|||||||
</span>
|
</span>
|
||||||
{isEdited && (
|
{isEdited && (
|
||||||
<span className="text-light-900 dark:text-dark-800">
|
<span className="text-light-900 dark:text-dark-800">
|
||||||
{" (edited)"}
|
{t` (edited)`}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
@@ -140,7 +141,7 @@ const Comment = ({
|
|||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<ContentEditable
|
<ContentEditable
|
||||||
placeholder="Add a comment..."
|
placeholder={t`Add a 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)}
|
||||||
@@ -152,14 +153,14 @@ const Comment = ({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setIsEditing(false)}
|
onClick={() => setIsEditing(false)}
|
||||||
>
|
>
|
||||||
Cancel
|
{t`Cancel`}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isLoading={updateCommentMutation.isPending}
|
isLoading={updateCommentMutation.isPending}
|
||||||
type="submit"
|
type="submit"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
Save
|
{t`Save`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
|
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
@@ -47,8 +48,8 @@ export function DeleteCardConfirmation({
|
|||||||
onError: (_error, _newList, context) => {
|
onError: (_error, _newList, context) => {
|
||||||
utils.board.byId.setData(queryParams, context?.previousState);
|
utils.board.byId.setData(queryParams, context?.previousState);
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Unable to delete card",
|
header: t`Unable to delete card`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -71,10 +72,10 @@ export function DeleteCardConfirmation({
|
|||||||
<div className="p-5">
|
<div className="p-5">
|
||||||
<div className="flex w-full flex-col justify-between pb-4">
|
<div className="flex w-full flex-col justify-between pb-4">
|
||||||
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
||||||
Are you sure you want to delete this card?
|
{t`Are you sure you want to delete this card?`}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
|
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
|
||||||
{"This action can't be undone."}
|
{t`This action can't be undone.`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex justify-end sm:mt-6">
|
<div className="mt-5 flex justify-end sm:mt-6">
|
||||||
@@ -82,13 +83,13 @@ export function DeleteCardConfirmation({
|
|||||||
className="mr-4 inline-flex justify-center rounded-md border-[1px] border-light-600 bg-light-50 px-3 py-2 text-sm font-semibold text-neutral-900 shadow-sm focus-visible:outline-none dark:border-dark-600 dark:bg-dark-300 dark:text-dark-1000"
|
className="mr-4 inline-flex justify-center rounded-md border-[1px] border-light-600 bg-light-50 px-3 py-2 text-sm font-semibold text-neutral-900 shadow-sm focus-visible:outline-none dark:border-dark-600 dark:bg-dark-300 dark:text-dark-1000"
|
||||||
onClick={() => closeModal()}
|
onClick={() => closeModal()}
|
||||||
>
|
>
|
||||||
Cancel
|
{t`Cancel`}
|
||||||
</button>
|
</button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleDeleteCard}
|
onClick={handleDeleteCard}
|
||||||
isLoading={deleteCardMutation.isPending}
|
isLoading={deleteCardMutation.isPending}
|
||||||
>
|
>
|
||||||
Delete
|
{t`Delete`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { t } from "@lingui/core/macro";
|
||||||
|
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
@@ -39,8 +41,8 @@ export function DeleteCommentConfirmation({
|
|||||||
onError: (_error, _newList, context) => {
|
onError: (_error, _newList, context) => {
|
||||||
utils.card.byId.setData(queryParams, context?.previousState);
|
utils.card.byId.setData(queryParams, context?.previousState);
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Unable to delete comment",
|
header: t`Unable to delete comment`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -60,10 +62,10 @@ export function DeleteCommentConfirmation({
|
|||||||
<div className="p-5">
|
<div className="p-5">
|
||||||
<div className="flex w-full flex-col justify-between pb-4">
|
<div className="flex w-full flex-col justify-between pb-4">
|
||||||
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
||||||
Are you sure you want to delete this comment?
|
{t`Are you sure you want to delete this comment?`}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
|
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
|
||||||
{"This action can't be undone."}
|
{t`This action can't be undone.`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex justify-end sm:mt-6">
|
<div className="mt-5 flex justify-end sm:mt-6">
|
||||||
@@ -71,13 +73,13 @@ export function DeleteCommentConfirmation({
|
|||||||
className="mr-4 inline-flex justify-center rounded-md border-[1px] border-light-600 bg-light-50 px-3 py-2 text-sm font-semibold text-neutral-900 shadow-sm focus-visible:outline-none dark:border-dark-600 dark:bg-dark-300 dark:text-dark-1000"
|
className="mr-4 inline-flex justify-center rounded-md border-[1px] border-light-600 bg-light-50 px-3 py-2 text-sm font-semibold text-neutral-900 shadow-sm focus-visible:outline-none dark:border-dark-600 dark:bg-dark-300 dark:text-dark-1000"
|
||||||
onClick={() => closeModal()}
|
onClick={() => closeModal()}
|
||||||
>
|
>
|
||||||
Cancel
|
{t`Cancel`}
|
||||||
</button>
|
</button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleDeleteComment}
|
onClick={handleDeleteComment}
|
||||||
isLoading={deleteCommentMutation.isPending}
|
isLoading={deleteCommentMutation.isPending}
|
||||||
>
|
>
|
||||||
Delete
|
{t`Delete`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import { HiEllipsisHorizontal, HiOutlineTrash } from "react-icons/hi2";
|
import { HiEllipsisHorizontal, HiOutlineTrash } from "react-icons/hi2";
|
||||||
|
|
||||||
import Dropdown from "~/components/Dropdown";
|
import Dropdown from "~/components/Dropdown";
|
||||||
@@ -10,7 +11,7 @@ export default function BoardDropdown() {
|
|||||||
<Dropdown
|
<Dropdown
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
label: "Delete card",
|
label: t`Delete card`,
|
||||||
action: () => openModal("DELETE_CARD"),
|
action: () => openModal("DELETE_CARD"),
|
||||||
icon: <HiOutlineTrash className="h-[16px] w-[16px] text-dark-900" />,
|
icon: <HiOutlineTrash className="h-[16px] w-[16px] text-dark-900" />,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Menu } from "@headlessui/react";
|
import { Menu } from "@headlessui/react";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import { HiMiniPlus } from "react-icons/hi2";
|
import { HiMiniPlus } from "react-icons/hi2";
|
||||||
|
|
||||||
import Badge from "~/components/Badge";
|
import Badge from "~/components/Badge";
|
||||||
@@ -68,8 +69,8 @@ export default function LabelSelector({
|
|||||||
onError: (_error, _newList, context) => {
|
onError: (_error, _newList, context) => {
|
||||||
utils.card.byId.setData({ cardPublicId }, context?.previousCard);
|
utils.card.byId.setData({ cardPublicId }, context?.previousCard);
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Unable to update labels",
|
header: t`Unable to update labels`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -94,7 +95,7 @@ export default function LabelSelector({
|
|||||||
}}
|
}}
|
||||||
handleEdit={(labelPublicId) => openModal("EDIT_LABEL", labelPublicId)}
|
handleEdit={(labelPublicId) => openModal("EDIT_LABEL", labelPublicId)}
|
||||||
handleCreate={() => openModal("NEW_LABEL")}
|
handleCreate={() => openModal("NEW_LABEL")}
|
||||||
createNewItemLabel="Create new label"
|
createNewItemLabel={t`Create new label`}
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
{selectedLabels.length ? (
|
{selectedLabels.length ? (
|
||||||
@@ -105,13 +106,16 @@ export default function LabelSelector({
|
|||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
))}
|
))}
|
||||||
<Menu.Button>
|
<Menu.Button>
|
||||||
<Badge value="Add label" iconLeft={<HiMiniPlus size={14} />} />
|
<Badge
|
||||||
|
value={t`Add label`}
|
||||||
|
iconLeft={<HiMiniPlus size={14} />}
|
||||||
|
/>
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<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">
|
<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" />
|
||||||
Add label
|
{t`Add label`}
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
)}
|
)}
|
||||||
</CheckboxDropdown>
|
</CheckboxDropdown>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Menu } from "@headlessui/react";
|
import { Menu } from "@headlessui/react";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
|
|
||||||
import CheckboxDropdown from "~/components/CheckboxDropdown";
|
import CheckboxDropdown from "~/components/CheckboxDropdown";
|
||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
@@ -48,8 +49,8 @@ export default function ListSelector({
|
|||||||
onError: (_error, _newList, context) => {
|
onError: (_error, _newList, context) => {
|
||||||
utils.card.byId.setData({ cardPublicId }, context?.previousCard);
|
utils.card.byId.setData({ cardPublicId }, context?.previousCard);
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Unable to update list",
|
header: t`Unable to update list`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Menu } from "@headlessui/react";
|
import { Menu } from "@headlessui/react";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import { HiMiniPlus } from "react-icons/hi2";
|
import { HiMiniPlus } from "react-icons/hi2";
|
||||||
|
|
||||||
import Avatar from "~/components/Avatar";
|
import Avatar from "~/components/Avatar";
|
||||||
@@ -55,6 +56,8 @@ export default function MemberSelector({
|
|||||||
...oldCard.members,
|
...oldCard.members,
|
||||||
{
|
{
|
||||||
publicId: update.workspaceMemberPublicId,
|
publicId: update.workspaceMemberPublicId,
|
||||||
|
email: memberToAdd?.email ?? "",
|
||||||
|
deletedAt: null,
|
||||||
user: {
|
user: {
|
||||||
id: memberToAdd?.user?.id ?? "",
|
id: memberToAdd?.user?.id ?? "",
|
||||||
name: memberToAdd?.user?.name ?? "",
|
name: memberToAdd?.user?.name ?? "",
|
||||||
@@ -73,8 +76,8 @@ export default function MemberSelector({
|
|||||||
onError: (_error, _newList, context) => {
|
onError: (_error, _newList, context) => {
|
||||||
utils.card.byId.setData({ cardPublicId }, context?.previousCard);
|
utils.card.byId.setData({ cardPublicId }, context?.previousCard);
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Unable to update members",
|
header: t`Unable to update members`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -106,7 +109,7 @@ export default function MemberSelector({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
handleCreate={handleInviteMember}
|
handleCreate={handleInviteMember}
|
||||||
createNewItemLabel="Invite member"
|
createNewItemLabel={t`Invite member`}
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<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">
|
<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">
|
||||||
@@ -124,7 +127,7 @@ export default function MemberSelector({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<HiMiniPlus size={22} className="pr-2" />
|
<HiMiniPlus size={22} className="pr-2" />
|
||||||
{"Add member"}
|
{t`Add member`}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import ContentEditable from "react-contenteditable";
|
import ContentEditable from "react-contenteditable";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { HiOutlineArrowUp } from "react-icons/hi2";
|
import { HiOutlineArrowUp } from "react-icons/hi2";
|
||||||
@@ -26,8 +27,8 @@ const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => {
|
|||||||
const addCommentMutation = api.card.addComment.useMutation({
|
const addCommentMutation = api.card.addComment.useMutation({
|
||||||
onError: (_error, _newList) => {
|
onError: (_error, _newList) => {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Unable to add comment",
|
header: t`Unable to add comment`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -53,7 +54,7 @@ const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => {
|
|||||||
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"
|
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="Add a comment..."
|
placeholder={t`Add a 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)}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { IoChevronForwardSharp } from "react-icons/io5";
|
import { IoChevronForwardSharp } from "react-icons/io5";
|
||||||
|
|
||||||
import Avatar from "~/components/Avatar";
|
import Avatar from "~/components/Avatar";
|
||||||
|
import Editor from "~/components/Editor";
|
||||||
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";
|
||||||
@@ -23,7 +25,6 @@ import LabelSelector from "./components/LabelSelector";
|
|||||||
import ListSelector from "./components/ListSelector";
|
import ListSelector from "./components/ListSelector";
|
||||||
import MemberSelector from "./components/MemberSelector";
|
import MemberSelector from "./components/MemberSelector";
|
||||||
import NewCommentForm from "./components/NewCommentForm";
|
import NewCommentForm from "./components/NewCommentForm";
|
||||||
import Editor from "~/components/Editor";
|
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
cardId: string;
|
cardId: string;
|
||||||
@@ -111,8 +112,8 @@ export default function CardPage() {
|
|||||||
const updateCard = api.card.update.useMutation({
|
const updateCard = api.card.update.useMutation({
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Unable to update card",
|
header: t`Unable to update card`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -144,7 +145,7 @@ export default function CardPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead
|
<PageHead
|
||||||
title={`${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="flex h-full w-full flex-col overflow-hidden">
|
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||||
@@ -189,7 +190,7 @@ export default function CardPage() {
|
|||||||
)}
|
)}
|
||||||
{!card && !isLoading && (
|
{!card && !isLoading && (
|
||||||
<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]">
|
<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]">
|
||||||
Card not found
|
{t`Card not found`}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -211,7 +212,7 @@ export default function CardPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="border-t-[1px] border-light-600 pt-12 dark:border-dark-400">
|
<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">
|
||||||
Activity
|
{t`Activity`}
|
||||||
</h2>
|
</h2>
|
||||||
<div>
|
<div>
|
||||||
<ActivityList
|
<ActivityList
|
||||||
@@ -231,7 +232,7 @@ export default function CardPage() {
|
|||||||
</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="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">
|
<div className="mb-4 flex w-full">
|
||||||
<p className="my-2 w-[100px] text-sm">List</p>
|
<p className="my-2 w-[100px] text-sm">{t`List`}</p>
|
||||||
<ListSelector
|
<ListSelector
|
||||||
cardPublicId={cardId}
|
cardPublicId={cardId}
|
||||||
lists={formattedLists}
|
lists={formattedLists}
|
||||||
@@ -239,7 +240,7 @@ export default function CardPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4 flex w-full">
|
<div className="mb-4 flex w-full">
|
||||||
<p className="my-2 w-[100px] text-sm">Labels</p>
|
<p className="my-2 w-[100px] text-sm">{t`Labels`}</p>
|
||||||
<LabelSelector
|
<LabelSelector
|
||||||
cardPublicId={cardId}
|
cardPublicId={cardId}
|
||||||
labels={formattedLabels}
|
labels={formattedLabels}
|
||||||
@@ -247,7 +248,7 @@ export default function CardPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full">
|
<div className="flex w-full">
|
||||||
<p className="my-2 w-[100px] text-sm">Members</p>
|
<p className="my-2 w-[100px] text-sm">{t`Members`}</p>
|
||||||
<MemberSelector
|
<MemberSelector
|
||||||
cardPublicId={cardId}
|
cardPublicId={cardId}
|
||||||
members={formattedMembers}
|
members={formattedMembers}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const Header = ({ isLoggedIn }: { isLoggedIn: boolean }) => {
|
|||||||
<button
|
<button
|
||||||
onClick={toggleMenu}
|
onClick={toggleMenu}
|
||||||
className="z-50 p-2 lg:hidden"
|
className="z-50 p-2 lg:hidden"
|
||||||
aria-label="Toggle menu"
|
aria-label={t`Toggle menu`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const Pricing = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-16 flex justify-center">
|
<div className="mt-16 flex justify-center">
|
||||||
<fieldset aria-label="Payment frequency">
|
<fieldset aria-label={t`Payment frequency`}>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={frequency}
|
value={frequency}
|
||||||
onChange={(value) => setFrequency(value)}
|
onChange={(value) => setFrequency(value)}
|
||||||
@@ -141,7 +141,7 @@ const Pricing = () => {
|
|||||||
>
|
>
|
||||||
{tier.name}
|
{tier.name}
|
||||||
</h3>
|
</h3>
|
||||||
{tier.highlighted && frequency.value === "annually" ? (
|
{tier.highlighted && frequency?.value === "annually" ? (
|
||||||
<p className="rounded-full bg-light-50 px-2.5 py-1 text-[12px] font-semibold text-dark-500 dark:bg-dark-1000 dark:text-dark-50">
|
<p className="rounded-full bg-light-50 px-2.5 py-1 text-[12px] font-semibold text-dark-500 dark:bg-dark-1000 dark:text-dark-50">
|
||||||
-20%
|
-20%
|
||||||
</p>
|
</p>
|
||||||
@@ -165,11 +165,11 @@ const Pricing = () => {
|
|||||||
!tier.showPrice && "opacity-0",
|
!tier.showPrice && "opacity-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{tier.price[frequency.value]}
|
{tier.price[frequency?.value || "monthly"]}
|
||||||
</span>
|
</span>
|
||||||
{tier.showPriceSuffix && (
|
{tier.showPriceSuffix && (
|
||||||
<span className="text-sm/6 font-semibold text-light-50 dark:text-dark-900">
|
<span className="text-sm/6 font-semibold text-light-50 dark:text-dark-900">
|
||||||
{frequency.priceSuffix}
|
{frequency?.priceSuffix}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { t } from "@lingui/core/macro";
|
||||||
|
import { Trans } from "@lingui/react/macro";
|
||||||
|
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
@@ -21,8 +24,8 @@ export function DeleteMemberConfirmation() {
|
|||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Unable to remove member",
|
header: t`Unable to remove member`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
closeModal();
|
closeModal();
|
||||||
@@ -41,18 +44,18 @@ export function DeleteMemberConfirmation() {
|
|||||||
<div className="p-5">
|
<div className="p-5">
|
||||||
<div className="flex w-full flex-col justify-between pb-4">
|
<div className="flex w-full flex-col justify-between pb-4">
|
||||||
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
||||||
{`Are you sure want to remove ${entityLabel}?`}
|
<Trans>Are you sure want to remove {entityLabel}?</Trans>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
|
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
|
||||||
{"They won't be able to access this workspace."}
|
{t`They won't be able to access this workspace.`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
|
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
|
||||||
<Button onClick={() => closeModal()} variant="secondary">
|
<Button onClick={() => closeModal()} variant="secondary">
|
||||||
Cancel
|
{t`Cancel`}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleDeleteMember} isLoading={deleteMember.isPending}>
|
<Button onClick={handleDeleteMember} isLoading={deleteMember.isPending}>
|
||||||
Remove
|
{t`Remove`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import { useEffect } 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";
|
||||||
@@ -46,7 +47,7 @@ export function InviteMemberForm() {
|
|||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="px-5 pt-5">
|
<div className="px-5 pt-5">
|
||||||
<div className="text-neutral-9000 flex w-full items-center justify-between pb-4 dark:text-dark-1000">
|
<div className="text-neutral-9000 flex w-full items-center justify-between pb-4 dark:text-dark-1000">
|
||||||
<h2 className="text-sm font-bold">Add member</h2>
|
<h2 className="text-sm font-bold">{t`Add member`}</h2>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="hover:bg-li ght-300 rounded p-1 focus:outline-none dark:hover:bg-dark-300"
|
className="hover:bg-li ght-300 rounded p-1 focus:outline-none dark:hover:bg-dark-300"
|
||||||
@@ -60,7 +61,7 @@ export function InviteMemberForm() {
|
|||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
placeholder="Email"
|
placeholder={t`Email`}
|
||||||
{...register("email", { required: true })}
|
{...register("email", { required: true })}
|
||||||
onKeyDown={async (e) => {
|
onKeyDown={async (e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
@@ -78,7 +79,7 @@ export function InviteMemberForm() {
|
|||||||
isLoading={createBoard.isPending}
|
isLoading={createBoard.isPending}
|
||||||
className="inline-flex w-full justify-center rounded-md bg-light-1000 px-3 py-2 text-sm font-semibold text-light-50 shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
|
className="inline-flex w-full justify-center rounded-md bg-light-1000 px-3 py-2 text-sm font-semibold text-light-50 shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
|
||||||
>
|
>
|
||||||
Invite member
|
{t`Invite member`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
@@ -103,7 +104,7 @@ export default function MembersPage() {
|
|||||||
</span>
|
</span>
|
||||||
{memberStatus === "invited" && (
|
{memberStatus === "invited" && (
|
||||||
<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">
|
<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">
|
||||||
Pending
|
{t`Pending`}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -116,7 +117,7 @@ export default function MembersPage() {
|
|||||||
<Dropdown
|
<Dropdown
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
label: "Remove member",
|
label: t`Remove member`,
|
||||||
action: () =>
|
action: () =>
|
||||||
openModal(
|
openModal(
|
||||||
"REMOVE_MEMBER",
|
"REMOVE_MEMBER",
|
||||||
@@ -140,11 +141,11 @@ export default function MembersPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title={`Members | ${workspace.name ?? "Workspace"}`} />
|
<PageHead title={t`Members | ${workspace.name ?? "Workspace"}`} />
|
||||||
<div className="m-auto max-w-[1600px] px-28 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]">
|
||||||
Members
|
{t`Members`}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Button
|
<Button
|
||||||
@@ -152,7 +153,7 @@ export default function MembersPage() {
|
|||||||
iconLeft={<HiOutlinePlusSmall className="h-4 w-4" />}
|
iconLeft={<HiOutlinePlusSmall className="h-4 w-4" />}
|
||||||
disabled={workspace.role !== "admin"}
|
disabled={workspace.role !== "admin"}
|
||||||
>
|
>
|
||||||
Invite
|
{t`Invite`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,13 +169,13 @@ export default function MembersPage() {
|
|||||||
scope="col"
|
scope="col"
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
User
|
{t`User`}
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="w-[35%] rounded-tr-lg px-3 py-3.5 text-left text-sm font-semibold text-light-900 dark:text-dark-900"
|
className="w-[35%] rounded-tr-lg px-3 py-3.5 text-left text-sm font-semibold text-light-900 dark:text-dark-900"
|
||||||
>
|
>
|
||||||
Role
|
{t`Role`}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { authClient } from "@kan/auth/client";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import { HiXMark } from "react-icons/hi2";
|
import { HiXMark } from "react-icons/hi2";
|
||||||
import Editor from "~/components/Editor";
|
|
||||||
|
|
||||||
|
import { authClient } from "@kan/auth/client";
|
||||||
|
|
||||||
|
import Editor from "~/components/Editor";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import ActivityList from "~/views/card/components/ActivityList";
|
import ActivityList from "~/views/card/components/ActivityList";
|
||||||
@@ -99,7 +101,7 @@ export function CardModal({
|
|||||||
)}
|
)}
|
||||||
<div className="border-t-[1px] border-light-600 pb-4 pt-12 dark:border-dark-400">
|
<div className="border-t-[1px] border-light-600 pb-4 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">
|
||||||
Activity
|
{t`Activity`}
|
||||||
</h2>
|
</h2>
|
||||||
<div>
|
<div>
|
||||||
{cardPublicId && (
|
{cardPublicId && (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import { keepPreviousData } from "@tanstack/react-query";
|
import { keepPreviousData } from "@tanstack/react-query";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { HiLink } from "react-icons/hi2";
|
import { HiLink } from "react-icons/hi2";
|
||||||
@@ -47,9 +48,9 @@ export default function PublicBoardView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Link copied",
|
header: t`Link copied`,
|
||||||
icon: "success",
|
icon: "success",
|
||||||
message: "Board URL copied to clipboard",
|
message: t`Board URL copied to clipboard`,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="rounded p-1.5 transition-all hover:bg-light-200 dark:hover:bg-dark-100"
|
className="rounded p-1.5 transition-all hover:bg-light-200 dark:hover:bg-dark-100"
|
||||||
@@ -82,7 +83,7 @@ export default function PublicBoardView() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead
|
<PageHead
|
||||||
title={`${data?.name ?? "Board"} | ${data?.workspace?.name ?? "Workspace"}`}
|
title={`${data?.name ?? "Board"} | ${data?.workspace.name ?? "Workspace"}`}
|
||||||
/>
|
/>
|
||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
html {
|
html {
|
||||||
@@ -137,7 +138,7 @@ export default function PublicBoardView() {
|
|||||||
{list.cards.map((card) => (
|
{list.cards.map((card) => (
|
||||||
<Link
|
<Link
|
||||||
key={card.publicId}
|
key={card.publicId}
|
||||||
href={`/${data.workspace?.slug}/${data.slug}/${card.publicId}`}
|
href={`/${data.workspace.slug}/${data.slug}/${card.publicId}`}
|
||||||
className={`mb-2 flex !cursor-pointer flex-col`}
|
className={`mb-2 flex !cursor-pointer flex-col`}
|
||||||
shallow={true}
|
shallow={true}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -177,7 +178,7 @@ export default function PublicBoardView() {
|
|||||||
<Modal modalSize={"md"} positionFromTop={"sm"}>
|
<Modal modalSize={"md"} positionFromTop={"sm"}>
|
||||||
<CardModal
|
<CardModal
|
||||||
cardPublicId={cardPublicId}
|
cardPublicId={cardPublicId}
|
||||||
workspaceSlug={data?.workspace?.slug}
|
workspaceSlug={data?.workspace.slug}
|
||||||
boardSlug={data?.slug}
|
boardSlug={data?.slug}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
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";
|
||||||
|
|
||||||
@@ -20,8 +21,8 @@ export default function Avatar({
|
|||||||
const updateUser = api.user.update.useMutation({
|
const updateUser = api.user.update.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Profile image updated",
|
header: t`Profile image updated`,
|
||||||
message: "Your profile image has been updated.",
|
message: t`Your profile image has been updated.`,
|
||||||
icon: "success",
|
icon: "success",
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
@@ -33,8 +34,8 @@ export default function Avatar({
|
|||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Error updating profile image",
|
header: t`Error updating profile image`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -50,8 +51,8 @@ export default function Avatar({
|
|||||||
|
|
||||||
if (!file || !userId) {
|
if (!file || !userId) {
|
||||||
return showPopup({
|
return showPopup({
|
||||||
header: "Error uploading profile image",
|
header: t`Error uploading profile image`,
|
||||||
message: "Please select a file to upload.",
|
message: t`Please select a file to upload.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -91,8 +92,8 @@ export default function Avatar({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Error uploading profile image",
|
header: t`Error uploading profile image`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { t } from "@lingui/core/macro";
|
||||||
|
|
||||||
import { authClient } from "@kan/auth/client";
|
import { authClient } from "@kan/auth/client";
|
||||||
|
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
@@ -44,12 +46,12 @@ const CreateAPIKeyForm = ({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button variant="danger" onClick={handleRevokeAPIKey}>
|
<Button variant="danger" onClick={handleRevokeAPIKey}>
|
||||||
Revoke
|
{t`Revoke`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button onClick={handleCreateAPIKey}>Create new key</Button>
|
<Button onClick={handleCreateAPIKey}>{t`Create new key`}</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { t } from "@lingui/core/macro";
|
||||||
|
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
|
|
||||||
@@ -37,19 +39,17 @@ export function CustomURLConfirmation({
|
|||||||
<div className="p-5">
|
<div className="p-5">
|
||||||
<div className="flex w-full flex-col justify-between pb-4">
|
<div className="flex w-full flex-col justify-between pb-4">
|
||||||
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
||||||
{`Confirm URL change`}
|
{t`Confirm URL change`}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
|
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
|
||||||
{
|
{t`Custom URLs are a premium feature. You'll be directed to upgrade your account.`}
|
||||||
"Custom URLs are a premium feature. You'll be directed to upgrade your account."
|
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
|
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
|
||||||
<Button onClick={() => closeModal()} variant="secondary">
|
<Button onClick={() => closeModal()} variant="secondary">
|
||||||
Cancel
|
{t`Cancel`}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleUpgrade}>Upgrade</Button>
|
<Button onClick={handleUpgrade}>{t`Upgrade`}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { authClient } from "@kan/auth/client";
|
|
||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { authClient } from "@kan/auth/client";
|
||||||
|
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
@@ -22,20 +24,20 @@ export function DeleteAccountConfirmation() {
|
|||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
closeModal();
|
closeModal();
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Account deleted",
|
header: t`Account deleted`,
|
||||||
message: "Your account has been deleted.",
|
message: t`Your account has been deleted.`,
|
||||||
icon: "success",
|
icon: "success",
|
||||||
});
|
});
|
||||||
|
|
||||||
utils.invalidate();
|
utils.invalidate();
|
||||||
|
|
||||||
router.push("/");
|
router.push("/");
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
closeModal();
|
closeModal();
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Error deleting account",
|
header: t`Error deleting account`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -49,14 +51,13 @@ export function DeleteAccountConfirmation() {
|
|||||||
<div className="p-5">
|
<div className="p-5">
|
||||||
<div className="flex w-full flex-col justify-between pb-4">
|
<div className="flex w-full flex-col justify-between pb-4">
|
||||||
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
||||||
{`Are you sure you want to delete your account?`}
|
{t`Are you sure you want to delete your account?`}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mb-4 text-sm text-light-900 dark:text-dark-900">
|
<p className="mb-4 text-sm text-light-900 dark:text-dark-900">
|
||||||
Keep in mind that this action is irreversible.
|
{t`Keep in mind that this action is irreversible.`}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-light-900 dark:text-dark-900">
|
<p className="text-sm text-light-900 dark:text-dark-900">
|
||||||
This will result in the permanent deletion of all data associated with
|
{t`This will result in the permanent deletion of all data associated with your account.`}
|
||||||
your account.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex items-start">
|
<div className="relative flex items-start">
|
||||||
@@ -78,14 +79,13 @@ export function DeleteAccountConfirmation() {
|
|||||||
id="comments-description"
|
id="comments-description"
|
||||||
className="text-light-900 dark:text-dark-1000"
|
className="text-light-900 dark:text-dark-1000"
|
||||||
>
|
>
|
||||||
I acknowledge that all of my account data will be permanently
|
{t`I acknowledge that all of my account data will be permanently deleted and want to proceed.`}
|
||||||
deleted and want to proceed.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
|
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
|
||||||
<Button variant="secondary" onClick={() => closeModal()}>
|
<Button variant="secondary" onClick={() => closeModal()}>
|
||||||
Cancel
|
{t`Cancel`}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -93,7 +93,7 @@ export function DeleteAccountConfirmation() {
|
|||||||
disabled={!isAcknowledgmentChecked}
|
disabled={!isAcknowledgmentChecked}
|
||||||
isLoading={deleteAccountMutation.isPending}
|
isLoading={deleteAccountMutation.isPending}
|
||||||
>
|
>
|
||||||
Delete account
|
{t`Delete account`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
@@ -21,13 +23,13 @@ export function DeleteWorkspaceConfirmation() {
|
|||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
closeModal();
|
closeModal();
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Workspace deleted",
|
header: t`Workspace deleted`,
|
||||||
message: "Your workspace has been deleted.",
|
message: t`Your workspace has been deleted.`,
|
||||||
icon: "success",
|
icon: "success",
|
||||||
});
|
});
|
||||||
|
|
||||||
await utils.workspace.all.refetch();
|
await utils.workspace.all.refetch();
|
||||||
|
|
||||||
const filteredWorkspaces = availableWorkspaces.filter(
|
const filteredWorkspaces = availableWorkspaces.filter(
|
||||||
(ws) => ws.publicId !== workspace.publicId,
|
(ws) => ws.publicId !== workspace.publicId,
|
||||||
);
|
);
|
||||||
@@ -40,8 +42,8 @@ export function DeleteWorkspaceConfirmation() {
|
|||||||
onError: () => {
|
onError: () => {
|
||||||
closeModal();
|
closeModal();
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Error deleting workspace",
|
header: t`Error deleting workspace`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -57,14 +59,15 @@ export function DeleteWorkspaceConfirmation() {
|
|||||||
<div className="p-5">
|
<div className="p-5">
|
||||||
<div className="flex w-full flex-col justify-between pb-4">
|
<div className="flex w-full flex-col justify-between pb-4">
|
||||||
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
||||||
{`Are you sure you want to delete the workspace ${workspace.name}?`}
|
<Trans>
|
||||||
|
Are you sure you want to delete the workspace {workspace.name}?
|
||||||
|
</Trans>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mb-4 text-sm text-light-900 dark:text-dark-900">
|
<p className="mb-4 text-sm text-light-900 dark:text-dark-900">
|
||||||
Keep in mind that this action is irreversible.
|
{t`Keep in mind that this action is irreversible.`}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-light-900 dark:text-dark-900">
|
<p className="text-sm text-light-900 dark:text-dark-900">
|
||||||
This will result in the permanent deletion of all data associated with
|
{t`This will result in the permanent deletion of all data associated with this workspace.`}
|
||||||
this workspace.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex items-start">
|
<div className="relative flex items-start">
|
||||||
@@ -86,14 +89,13 @@ export function DeleteWorkspaceConfirmation() {
|
|||||||
id="comments-description"
|
id="comments-description"
|
||||||
className="text-light-900 dark:text-dark-1000"
|
className="text-light-900 dark:text-dark-1000"
|
||||||
>
|
>
|
||||||
I acknowledge that all of the workspace data will be permanently
|
{t`I acknowledge that all of the workspace data will be permanently deleted and want to proceed.`}
|
||||||
deleted and want to proceed.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
|
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
|
||||||
<Button variant="secondary" onClick={() => closeModal()}>
|
<Button variant="secondary" onClick={() => closeModal()}>
|
||||||
Cancel
|
{t`Cancel`}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -101,7 +103,7 @@ export function DeleteWorkspaceConfirmation() {
|
|||||||
disabled={!isAcknowledgmentChecked}
|
disabled={!isAcknowledgmentChecked}
|
||||||
isLoading={deleteWorkspaceMutation.isPending}
|
isLoading={deleteWorkspaceMutation.isPending}
|
||||||
>
|
>
|
||||||
Delete workspace
|
{t`Delete workspace`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -11,10 +12,10 @@ const schema = z.object({
|
|||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
.min(3, {
|
.min(3, {
|
||||||
message: "Display namemust be at least 3 characters long",
|
message: t`Display name must be at least 3 characters long`,
|
||||||
})
|
})
|
||||||
.max(280, {
|
.max(280, {
|
||||||
message: "Display name cannot exceed 280 characters",
|
message: t`Display name cannot exceed 280 characters`,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -37,8 +38,8 @@ const UpdateDisplayNameForm = ({ displayName }: { displayName: string }) => {
|
|||||||
const updateDisplayName = api.user.update.useMutation({
|
const updateDisplayName = api.user.update.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Display name updated",
|
header: t`Display name updated`,
|
||||||
message: "Your display name has been updated.",
|
message: t`Your display name has been updated.`,
|
||||||
icon: "success",
|
icon: "success",
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
@@ -50,8 +51,8 @@ const UpdateDisplayNameForm = ({ displayName }: { displayName: string }) => {
|
|||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Error updating display name",
|
header: t`Error updating display name`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -75,7 +76,7 @@ const UpdateDisplayNameForm = ({ displayName }: { displayName: string }) => {
|
|||||||
disabled={!isDirty || updateDisplayName.isPending}
|
disabled={!isDirty || updateDisplayName.isPending}
|
||||||
isLoading={updateDisplayName.isPending}
|
isLoading={updateDisplayName.isPending}
|
||||||
>
|
>
|
||||||
Update
|
{t`Update`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -7,19 +8,6 @@ import Input from "~/components/Input";
|
|||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
description: z
|
|
||||||
.string()
|
|
||||||
.min(3, {
|
|
||||||
message: "Workspace description must be at least 3 characters long",
|
|
||||||
})
|
|
||||||
.max(280, {
|
|
||||||
message: "Workspace description cannot exceed 280 characters",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
type FormValues = z.infer<typeof schema>;
|
|
||||||
|
|
||||||
const UpdateWorkspaceDescriptionForm = ({
|
const UpdateWorkspaceDescriptionForm = ({
|
||||||
workspacePublicId,
|
workspacePublicId,
|
||||||
workspaceDescription,
|
workspaceDescription,
|
||||||
@@ -29,6 +17,20 @@ const UpdateWorkspaceDescriptionForm = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.min(3, {
|
||||||
|
message: t`Workspace description must be at least 3 characters long`,
|
||||||
|
})
|
||||||
|
.max(280, {
|
||||||
|
message: t`Workspace description cannot exceed 280 characters`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormValues = z.infer<typeof schema>;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -43,8 +45,8 @@ const UpdateWorkspaceDescriptionForm = ({
|
|||||||
const updateWorkspaceDescription = api.workspace.update.useMutation({
|
const updateWorkspaceDescription = api.workspace.update.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Workspace description updated",
|
header: t`Workspace description updated`,
|
||||||
message: "Your workspace description has been updated.",
|
message: t`Your workspace description has been updated.`,
|
||||||
icon: "success",
|
icon: "success",
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
@@ -56,8 +58,8 @@ const UpdateWorkspaceDescriptionForm = ({
|
|||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Error updating workspace description",
|
header: t`Error updating workspace description`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -85,7 +87,7 @@ const UpdateWorkspaceDescriptionForm = ({
|
|||||||
disabled={!isDirty || updateWorkspaceDescription.isPending}
|
disabled={!isDirty || updateWorkspaceDescription.isPending}
|
||||||
isLoading={updateWorkspaceDescription.isPending}
|
isLoading={updateWorkspaceDescription.isPending}
|
||||||
>
|
>
|
||||||
Update
|
{t`Update`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -10,8 +11,8 @@ import { api } from "~/utils/api";
|
|||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
.min(3, { message: "Workspace name must be at least 3 characters long" })
|
.min(3, { message: t`Workspace name must be at least 3 characters long` })
|
||||||
.max(24, { message: "Workspace name cannot exceed 24 characters" }),
|
.max(24, { message: t`Workspace name cannot exceed 24 characters` }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormValues = z.infer<typeof schema>;
|
type FormValues = z.infer<typeof schema>;
|
||||||
@@ -39,8 +40,8 @@ const UpdateWorkspaceNameForm = ({
|
|||||||
const updateWorkspaceName = api.workspace.update.useMutation({
|
const updateWorkspaceName = api.workspace.update.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Workspace name updated",
|
header: t`Workspace name updated`,
|
||||||
message: "Your workspace name has been updated.",
|
message: t`Your workspace name has been updated.`,
|
||||||
icon: "success",
|
icon: "success",
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
@@ -52,8 +53,8 @@ const UpdateWorkspaceNameForm = ({
|
|||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Error updating workspace name",
|
header: t`Error updating workspace name`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -78,7 +79,7 @@ const UpdateWorkspaceNameForm = ({
|
|||||||
disabled={!isDirty || updateWorkspaceName.isPending}
|
disabled={!isDirty || updateWorkspaceName.isPending}
|
||||||
isLoading={updateWorkspaceName.isPending}
|
isLoading={updateWorkspaceName.isPending}
|
||||||
>
|
>
|
||||||
Update
|
{t`Update`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import { env } from "next-runtime-env";
|
import { env } from "next-runtime-env";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { HiCheck, HiMiniStar } from "react-icons/hi2";
|
import { HiCheck, HiMiniStar } from "react-icons/hi2";
|
||||||
@@ -11,20 +12,6 @@ import { useModal } from "~/providers/modal";
|
|||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
slug: z
|
|
||||||
.string()
|
|
||||||
.min(3, {
|
|
||||||
message: "URL must be at least 3 characters long",
|
|
||||||
})
|
|
||||||
.max(24, { message: "URL cannot exceed 24 characters" })
|
|
||||||
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/, {
|
|
||||||
message: "URL can only contain letters, numbers, and hyphens",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
type FormValues = z.infer<typeof schema>;
|
|
||||||
|
|
||||||
const UpdateWorkspaceUrlForm = ({
|
const UpdateWorkspaceUrlForm = ({
|
||||||
workspacePublicId,
|
workspacePublicId,
|
||||||
workspaceUrl,
|
workspaceUrl,
|
||||||
@@ -37,6 +24,21 @@ const UpdateWorkspaceUrlForm = ({
|
|||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
const { openModal } = useModal();
|
const { openModal } = useModal();
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
slug: z
|
||||||
|
.string()
|
||||||
|
.min(3, {
|
||||||
|
message: t`URL must be at least 3 characters long`,
|
||||||
|
})
|
||||||
|
.max(24, { message: t`URL cannot exceed 24 characters` })
|
||||||
|
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/, {
|
||||||
|
message: t`URL can only contain letters, numbers, and hyphens`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormValues = z.infer<typeof schema>;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -55,8 +57,8 @@ const UpdateWorkspaceUrlForm = ({
|
|||||||
const updateWorkspaceSlug = api.workspace.update.useMutation({
|
const updateWorkspaceSlug = api.workspace.update.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Workspace slug updated",
|
header: t`Workspace slug updated`,
|
||||||
message: "Your workspace slug has been updated.",
|
message: t`Your workspace slug has been updated.`,
|
||||||
icon: "success",
|
icon: "success",
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
@@ -68,8 +70,8 @@ const UpdateWorkspaceUrlForm = ({
|
|||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Error updating workspace URL",
|
header: t`Error updating workspace URL`,
|
||||||
message: "Please try again later, or contact customer support.",
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -118,7 +120,7 @@ const UpdateWorkspaceUrlForm = ({
|
|||||||
errorMessage={
|
errorMessage={
|
||||||
errors.slug?.message ||
|
errors.slug?.message ||
|
||||||
(isWorkspaceSlugAvailable?.isAvailable === false
|
(isWorkspaceSlugAvailable?.isAvailable === false
|
||||||
? "This workspace username has already been taken"
|
? t`This workspace username has already been taken`
|
||||||
: undefined)
|
: undefined)
|
||||||
}
|
}
|
||||||
prefix={
|
prefix={
|
||||||
@@ -149,7 +151,7 @@ const UpdateWorkspaceUrlForm = ({
|
|||||||
}
|
}
|
||||||
isLoading={updateWorkspaceSlug.isPending}
|
isLoading={updateWorkspaceSlug.isPending}
|
||||||
>
|
>
|
||||||
Update
|
{t`Update`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { t } from "@lingui/core/macro";
|
||||||
import { env } from "next-runtime-env";
|
import { env } from "next-runtime-env";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { HiMiniArrowTopRightOnSquare } from "react-icons/hi2";
|
import { HiMiniArrowTopRightOnSquare } from "react-icons/hi2";
|
||||||
@@ -65,15 +66,15 @@ export default function SettingsPage() {
|
|||||||
refetchIntegrations();
|
refetchIntegrations();
|
||||||
refetchTrelloUrl();
|
refetchTrelloUrl();
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Trello disconnected",
|
header: t`Trello disconnected`,
|
||||||
message: "Your Trello account has been disconnected.",
|
message: t`Your Trello account has been disconnected.`,
|
||||||
icon: "success",
|
icon: "success",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: "Error disconnecting Trello",
|
header: t`Error disconnecting Trello`,
|
||||||
message: "An error occurred while disconnecting your Trello account.",
|
message: t`An error occurred while disconnecting your Trello account.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -104,29 +105,29 @@ export default function SettingsPage() {
|
|||||||
<>
|
<>
|
||||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||||
<div className="h-full max-h-[calc(100vh-4rem)] overflow-y-auto">
|
<div className="h-full max-h-[calc(100vh-4rem)] overflow-y-auto">
|
||||||
<PageHead title={`Settings | ${workspace.name ?? "Workspace"}`} />
|
<PageHead title={t`Settings | ${workspace.name ?? "Workspace"}`} />
|
||||||
<div className="px-28 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]">
|
||||||
Settings
|
{t`Settings`}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
||||||
<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">
|
||||||
Profile picture
|
{t`Profile picture`}
|
||||||
</h2>
|
</h2>
|
||||||
<Avatar userId={data?.id} userImage={data?.image} />
|
<Avatar userId={data?.id} userImage={data?.image} />
|
||||||
|
|
||||||
<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">
|
||||||
Display name
|
{t`Display name`}
|
||||||
</h2>
|
</h2>
|
||||||
<UpdateDisplayNameForm displayName={data?.name ?? ""} />
|
<UpdateDisplayNameForm displayName={data?.name ?? ""} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
||||||
<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">
|
||||||
Workspace name
|
{t`Workspace name`}
|
||||||
</h2>
|
</h2>
|
||||||
<UpdateWorkspaceNameForm
|
<UpdateWorkspaceNameForm
|
||||||
workspacePublicId={workspace.publicId}
|
workspacePublicId={workspace.publicId}
|
||||||
@@ -134,16 +135,16 @@ export default function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<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">
|
||||||
Workspace URL
|
{t`Workspace URL`}
|
||||||
</h2>
|
</h2>
|
||||||
<UpdateWorkspaceUrlForm
|
<UpdateWorkspaceUrlForm
|
||||||
workspacePublicId={workspace.publicId}
|
workspacePublicId={workspace.publicId}
|
||||||
workspaceUrl={workspace.slug}
|
workspaceUrl={workspace.slug ?? ""}
|
||||||
workspacePlan={workspace.plan}
|
workspacePlan={workspace.plan ?? "free"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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">
|
||||||
Workspace description
|
{t`Workspace description`}
|
||||||
</h2>
|
</h2>
|
||||||
<UpdateWorkspaceDescriptionForm
|
<UpdateWorkspaceDescriptionForm
|
||||||
workspacePublicId={workspace.publicId}
|
workspacePublicId={workspace.publicId}
|
||||||
@@ -153,10 +154,10 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
||||||
<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">
|
||||||
Language
|
{t`Language`}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
|
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
|
||||||
Change the language of the app.
|
{t`Change the language of the app.`}
|
||||||
</p>
|
</p>
|
||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
</div>
|
</div>
|
||||||
@@ -164,17 +165,17 @@ export default function SettingsPage() {
|
|||||||
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" && (
|
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" && (
|
||||||
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
||||||
<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">
|
||||||
Billing
|
{t`Billing`}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
|
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
|
||||||
View and manage your billing and subscription.
|
{t`View and manage your billing and subscription.`}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
iconRight={<HiMiniArrowTopRightOnSquare />}
|
iconRight={<HiMiniArrowTopRightOnSquare />}
|
||||||
onClick={handleOpenBillingPortal}
|
onClick={handleOpenBillingPortal}
|
||||||
>
|
>
|
||||||
Billing portal
|
{t`Billing portal`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -188,7 +189,7 @@ export default function SettingsPage() {
|
|||||||
) && trelloUrl ? (
|
) && trelloUrl ? (
|
||||||
<>
|
<>
|
||||||
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
|
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
|
||||||
Connect your Trello account to import boards.
|
{t`Connect your Trello account to import boards.`}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@@ -201,32 +202,34 @@ export default function SettingsPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Connect Trello
|
{t`Connect Trello`}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
integrations?.some(
|
||||||
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
|
(integration) => integration.provider === "trello",
|
||||||
You are already connected to Trello.
|
) && (
|
||||||
</p>
|
<>
|
||||||
<Button
|
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
|
||||||
variant="primary"
|
{t`Your Trello account is connected.`}
|
||||||
onClick={() => {
|
</p>
|
||||||
disconnectTrello({ provider: "trello" });
|
<Button
|
||||||
}}
|
variant="secondary"
|
||||||
>
|
onClick={() => disconnectTrello({ provider: "trello" })}
|
||||||
Disconnect Trello
|
>
|
||||||
</Button>
|
{t`Disconnect Trello`}
|
||||||
</>
|
</Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
||||||
<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">
|
||||||
API keys
|
{t`API keys`}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
|
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
|
||||||
View and manage your API keys.
|
{t`View and manage your API keys.`}
|
||||||
</p>
|
</p>
|
||||||
<CreateAPIKeyForm
|
<CreateAPIKeyForm
|
||||||
apiKey={data?.apiKey}
|
apiKey={data?.apiKey}
|
||||||
@@ -236,36 +239,35 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
||||||
<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">
|
||||||
Delete workspace
|
{t`Delete workspace`}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
|
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
|
||||||
Once you delete your workspace, there is no going back. This
|
{t`Once you delete your workspace, there is no going back. This action cannot be undone.`}
|
||||||
action cannot be undone.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => openModal("DELETE_WORKSPACE")}
|
onClick={() => openModal("DELETE_WORKSPACE")}
|
||||||
|
disabled={workspace.role !== "admin"}
|
||||||
>
|
>
|
||||||
Delete workspace
|
{t`Delete workspace`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
||||||
<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">
|
||||||
Delete account
|
{t`Delete account`}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
|
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
|
||||||
Once you delete your account, there is no going back. This
|
{t`Once you delete your account, there is no going back. This action cannot be undone.`}
|
||||||
action cannot be undone.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => openModal("DELETE_ACCOUNT")}
|
onClick={() => openModal("DELETE_ACCOUNT")}
|
||||||
>
|
>
|
||||||
Delete account
|
{t`Delete account`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user