mirror of
https://github.com/docmost/docmost.git
synced 2025-11-16 08:31:14 +10:00
feat: support i18n
This commit is contained in:
@ -25,3 +25,9 @@ If you are developing a production application, we recommend updating the config
|
||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
||||
|
||||
## Add i18n namespaces
|
||||
|
||||
`node scripts/i18n-tools.js add-ns <namespcaes>`
|
||||
|
||||
For example `node scripts/i18n-tools.js add-ns login`
|
||||
|
||||
@ -27,6 +27,9 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next-http-backend": "^2.6.1",
|
||||
"jotai": "^2.9.3",
|
||||
"jotai-optics": "^0.4.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
@ -41,6 +44,7 @@
|
||||
"react-drawio": "^0.2.0",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-moveable": "^0.56.0",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"socket.io-client": "^4.7.5",
|
||||
|
||||
11
apps/client/public/locales/en/invite-signup.json
Normal file
11
apps/client/public/locales/en/invite-signup.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"Join the workspace": "Join the workspace",
|
||||
"Name": "Name",
|
||||
"enter your full name": "enter your full name",
|
||||
"Email": "Email",
|
||||
"Password": "Password",
|
||||
"Your password": "Your password",
|
||||
"Sign Up": "Sign Up",
|
||||
"invalid invitation link": "invalid invitation link",
|
||||
"Invitation signup": "Invitation signup"
|
||||
}
|
||||
7
apps/client/public/locales/en/login.json
Normal file
7
apps/client/public/locales/en/login.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"Login": "Login",
|
||||
"Email": "Email",
|
||||
"Password": "Password",
|
||||
"Your password": "Your password",
|
||||
"Sign In": "Sign In"
|
||||
}
|
||||
138
apps/client/public/locales/en/settings.json
Normal file
138
apps/client/public/locales/en/settings.json
Normal file
@ -0,0 +1,138 @@
|
||||
{
|
||||
"account": {
|
||||
"My Profile": "My Profile",
|
||||
"Change photo": "Change photo",
|
||||
"Name": "Name",
|
||||
"Your name": "Your name",
|
||||
"Save": "Save",
|
||||
"Updated successfully": "Updated successfully",
|
||||
"Failed to update data": "Failed to update data",
|
||||
"Email": "Email",
|
||||
"Change email": "Change email",
|
||||
"To change your email, you have to enter your password and new email.": "To change your email, you have to enter your password and new email.",
|
||||
"Password": "Password",
|
||||
"Enter your password": "Enter your password",
|
||||
"Enter your new preferred email": "Enter your new preferred email",
|
||||
"New email": "New email",
|
||||
"You can change your password here.": "You can change your password here.",
|
||||
"Change password": "Change password",
|
||||
"Your password must be a minimum of 8 characters.": "Your password must be a minimum of 8 characters.",
|
||||
"Current password": "Current password",
|
||||
"Enter your current password": "Enter your current password",
|
||||
"New password": "New password",
|
||||
"Enter your new password": "Enter your new password",
|
||||
"Password changed successfully": "Password changed successfully"
|
||||
},
|
||||
"preference": {
|
||||
"Preferences": "Preferences",
|
||||
"Theme": "Theme",
|
||||
"Choose your preferred color scheme.": "Choose your preferred color scheme.",
|
||||
"Select theme": "Select theme",
|
||||
"Light": "Light",
|
||||
"Dark": "Dark",
|
||||
"System settings": "System settings",
|
||||
"Language": "Language",
|
||||
"Choose your preferred interface language.": "Choose your preferred interface language.",
|
||||
"Select language": "Select language",
|
||||
"Full page width": "Full page width",
|
||||
"Choose your preferred page width.": "Choose your preferred page width.",
|
||||
"Toggle full page width": "Toggle full page width"
|
||||
},
|
||||
"workspace": {
|
||||
"general": {
|
||||
"General": "General",
|
||||
"Name": "Name",
|
||||
"e.g ACME": "e.g ACME",
|
||||
"Save": "Save",
|
||||
"Updated successfully": "Updated successfully",
|
||||
"Failed to update data": "Failed to update data"
|
||||
},
|
||||
"member": {
|
||||
"Members": "Members",
|
||||
"Invite members": "Invite members",
|
||||
"Invite new members": "Invite new members",
|
||||
"Pending": "Pending",
|
||||
"User": "User",
|
||||
"Status": "Status",
|
||||
"Role": "Role",
|
||||
"Active": "Active",
|
||||
"Email": "Email",
|
||||
"Date": "Date",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Invited members who are yet to accept their invitation will appear here.",
|
||||
"Invite by email": "Invite by email",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Enter valid email addresses separated by comma or space [max: 50]",
|
||||
"enter valid emails addresses": "enter valid emails addresses",
|
||||
"Select role": "Select role",
|
||||
"Select role to assign to all invited members": "Select role to assign to all invited members",
|
||||
"Choose a role": "Choose a role",
|
||||
"Add to groups": "Add to groups",
|
||||
"Invited members will be granted access to spaces the groups can access": "Invited members will be granted access to spaces the groups can access",
|
||||
"Send invitation": "Send invitation"
|
||||
},
|
||||
"group": {
|
||||
"Groups": "Groups",
|
||||
"Create group": "Create group",
|
||||
"Group": "Group",
|
||||
"Members": "Members",
|
||||
"member": "member",
|
||||
"members": "members",
|
||||
"Manage Group": "Manage Group",
|
||||
"addGroupMembers": "addGroupMembers",
|
||||
"add": "add",
|
||||
"Edit group": "Edit group",
|
||||
"Group name": "Group name",
|
||||
"e.g Developers": "e.g Developers",
|
||||
"Group description": "Group description",
|
||||
"e.g Group for developers": "e.g Group for developers",
|
||||
"Edit": "Edit",
|
||||
"Delete group": "Delete group",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Are you sure you want to delete this group? Members will lose access to resources this group has access to.",
|
||||
"Delete": "Delete",
|
||||
"Cancel": "Cancel",
|
||||
"Remove group member": "Remove group member",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.",
|
||||
"Add group members": "Add group members",
|
||||
"Create": "Create",
|
||||
"User": "User",
|
||||
"Status": "Status",
|
||||
"Active": "Active",
|
||||
"Add members": "Add members",
|
||||
"Search for users": "Search for users",
|
||||
"No user found": "No user found"
|
||||
},
|
||||
"space": {
|
||||
"Spaces": "Spaces",
|
||||
"Create space": "Create space",
|
||||
"Space": "Space",
|
||||
"Members": "Members",
|
||||
"Settings": "Settings",
|
||||
"Details": "Details",
|
||||
"Name": "Name",
|
||||
"e.g Sales": "e.g Sales",
|
||||
"Slug": "Slug",
|
||||
"Description": "Description",
|
||||
"e.g Space for sales team to collaborate": "e.g Space for sales team to collaborate",
|
||||
"Save": "Save",
|
||||
"Space name": "Space name",
|
||||
"e.g Product Team": "e.g Product Team",
|
||||
"Space slug": "Space slug",
|
||||
"e.g product": "e.g product",
|
||||
"Space description": "Space description",
|
||||
"e.g Space for product team": "e.g Space for product team",
|
||||
"Create": "Create",
|
||||
"addSpaceMembers": "addSpaceMembers",
|
||||
"add": "add",
|
||||
"selectRole": "selectRole",
|
||||
"Remove space member": "Remove space member",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Are you sure you want to remove this user from the space? The user will lose all access to this space.",
|
||||
"Remove": "Remove",
|
||||
"Cancel": "Cancel",
|
||||
"Member": "Member",
|
||||
"Role": "Role",
|
||||
"Failed to load page. An error occurred.": "Failed to load page. An error occurred.",
|
||||
"Add members": "Add members",
|
||||
"Search for users and groups": "Search for users and groups",
|
||||
"No user found": "No user found"
|
||||
}
|
||||
}
|
||||
}
|
||||
11
apps/client/public/locales/en/setup-workspace.json
Normal file
11
apps/client/public/locales/en/setup-workspace.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"Setup workspace": "Setup workspace",
|
||||
"Create workspace": "Create workspace",
|
||||
"Workspace Name": "Workspace Name",
|
||||
"e.g ACME Inc": "e.g ACME Inc",
|
||||
"Your Name": "Your Name",
|
||||
"enter your full name": "enter your full name",
|
||||
"Your Email": "Your Email",
|
||||
"Password": "Password",
|
||||
"Enter a strong password": "Enter a strong password"
|
||||
}
|
||||
55
apps/client/public/locales/en/translation.json
Normal file
55
apps/client/public/locales/en/translation.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"common": {
|
||||
"Failed to fetch recent pages": "Failed to fetch recent pages",
|
||||
"Untitled": "Untitled",
|
||||
"No pages yet": "No pages yet",
|
||||
"Failed to load page. An error occurred.": "Failed to load page. An error occurred."
|
||||
},
|
||||
"layout": {
|
||||
"Home": "Home",
|
||||
"Workspace": "Workspace",
|
||||
"Workspace settings": "Workspace settings",
|
||||
"Manage members": "Manage members",
|
||||
"Account": "Account",
|
||||
"My profile": "My profile",
|
||||
"My preferences": "My preferences",
|
||||
"Logout": "Logout",
|
||||
"Settings": "Settings",
|
||||
"Profile": "Profile",
|
||||
"Preferences": "Preferences",
|
||||
"General": "General",
|
||||
"Members": "Members",
|
||||
"Groups": "Groups",
|
||||
"Spaces": "Spaces"
|
||||
},
|
||||
"home": {
|
||||
"Recently updated": "Recently updated"
|
||||
},
|
||||
"space": {
|
||||
"Spaces you belong to": "Spaces you belong to",
|
||||
"Recently updated": "Recently updated"
|
||||
},
|
||||
"page": {
|
||||
"Error fetching page data.": "Error fetching page data.",
|
||||
"untitled": "untitled",
|
||||
"Link copied": "Link copied",
|
||||
"Copy link": "Copy link",
|
||||
"Full width": "Full width",
|
||||
"Page history": "Page history",
|
||||
"Export": "Export",
|
||||
"Print PDF": "Print PDF",
|
||||
"Delete": "Delete"
|
||||
},
|
||||
"page-history": {
|
||||
"Page history": "Page history",
|
||||
"Error loading page history.": "Error loading page history.",
|
||||
"No page history saved yet.": "No page history saved yet.",
|
||||
"Please confirm your action": "Please confirm your action",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Are you sure you want to restore this version? Any changes not versioned will be lost.",
|
||||
"Confirm": "Confirm",
|
||||
"Cancel": "Cancel",
|
||||
"Successfully restored": "Successfully restored",
|
||||
"Restore": "Restore",
|
||||
"Error fetching page data.": "Error fetching page data."
|
||||
}
|
||||
}
|
||||
11
apps/client/public/locales/zh/invite-signup.json
Normal file
11
apps/client/public/locales/zh/invite-signup.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"Join the workspace": "加入工作空间",
|
||||
"Name": "姓名",
|
||||
"enter your full name": "输入您的全名",
|
||||
"Email": "电子邮箱",
|
||||
"Password": "密码",
|
||||
"Your password": "您的密码",
|
||||
"Sign Up": "注册",
|
||||
"invalid invitation link": "无效的邀请链接",
|
||||
"Invitation signup": "邀请注册"
|
||||
}
|
||||
7
apps/client/public/locales/zh/login.json
Normal file
7
apps/client/public/locales/zh/login.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"Login": "登录",
|
||||
"Email": "电子邮箱",
|
||||
"Password": "密码",
|
||||
"Your password": "您的密码",
|
||||
"Sign In": "登录"
|
||||
}
|
||||
138
apps/client/public/locales/zh/settings.json
Normal file
138
apps/client/public/locales/zh/settings.json
Normal file
@ -0,0 +1,138 @@
|
||||
{
|
||||
"account": {
|
||||
"My Profile": "我的个人资料",
|
||||
"Change photo": "更改照片",
|
||||
"Name": "姓名",
|
||||
"Your name": "您的姓名",
|
||||
"Save": "保存",
|
||||
"Updated successfully": "更新成功",
|
||||
"Failed to update data": "数据更新失败",
|
||||
"Email": "电子邮箱",
|
||||
"Change email": "更改电子邮箱",
|
||||
"To change your email, you have to enter your password and new email.": "要更改您的电子邮箱,您需要输入密码和新的电子邮箱地址。",
|
||||
"Password": "密码",
|
||||
"Enter your password": "输入您的密码",
|
||||
"Enter your new preferred email": "输入您新的首选电子邮箱",
|
||||
"New email": "新电子邮箱",
|
||||
"You can change your password here.": "您可以在这里更改密码。",
|
||||
"Change password": "更改密码",
|
||||
"Your password must be a minimum of 8 characters.": "您的密码必须至少包含8个字符。",
|
||||
"Current password": "当前密码",
|
||||
"Enter your current password": "输入您的当前密码",
|
||||
"New password": "新密码",
|
||||
"Enter your new password": "输入您的新密码",
|
||||
"Password changed successfully": "密码更改成功"
|
||||
},
|
||||
"preference": {
|
||||
"Preferences": "偏好设置",
|
||||
"Theme": "主题",
|
||||
"Choose your preferred color scheme.": "选择您喜欢的配色方案。",
|
||||
"Select theme": "选择主题",
|
||||
"Light": "浅色",
|
||||
"Dark": "深色",
|
||||
"System settings": "系统设置",
|
||||
"Language": "语言",
|
||||
"Choose your preferred interface language.": "选择您喜欢的界面语言。",
|
||||
"Select language": "选择语言",
|
||||
"Full page width": "全页宽度",
|
||||
"Choose your preferred page width.": "选择您喜欢的页面宽度。",
|
||||
"Toggle full page width": "切换全页宽度"
|
||||
},
|
||||
"workspace": {
|
||||
"general": {
|
||||
"General": "常规",
|
||||
"Name": "名称",
|
||||
"e.g ACME": "例如:ACME",
|
||||
"Save": "保存",
|
||||
"Updated successfully": "更新成功",
|
||||
"Failed to update data": "数据更新失败"
|
||||
},
|
||||
"member": {
|
||||
"Members": "成员",
|
||||
"Invite members": "邀请成员",
|
||||
"Invite new members": "邀请新成员",
|
||||
"Pending": "待定",
|
||||
"User": "用户",
|
||||
"Status": "状态",
|
||||
"Role": "角色",
|
||||
"Active": "活跃",
|
||||
"Email": "电子邮箱",
|
||||
"Date": "日期",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "尚未接受邀请的成员将显示在这里。",
|
||||
"Invite by email": "通过电子邮箱邀请",
|
||||
"Enter valid email addresses separated by comma or space max_50": "输入有效的电子邮箱地址,用逗号或空格分隔 [最多:50个]",
|
||||
"enter valid emails addresses": "输入有效的电子邮箱地址",
|
||||
"Select role": "选择角色",
|
||||
"Select role to assign to all invited members": "选择要分配给所有被邀请成员的角色",
|
||||
"Choose a role": "选择一个角色",
|
||||
"Add to groups": "添加到群组",
|
||||
"Invited members will be granted access to spaces the groups can access": "被邀请的成员将被授予访问群组可以访问的空间的权限",
|
||||
"Send invitation": "发送邀请"
|
||||
},
|
||||
"group": {
|
||||
"Groups": "群组",
|
||||
"Create group": "创建群组",
|
||||
"Group": "群组",
|
||||
"Members": "成员",
|
||||
"member": "成员",
|
||||
"members": "成员",
|
||||
"Manage Group": "管理群组",
|
||||
"addGroupMembers": "添加群组成员",
|
||||
"add": "添加",
|
||||
"Edit group": "编辑群组",
|
||||
"Group name": "群组名称",
|
||||
"e.g Developers": "例如:开发人员",
|
||||
"Group description": "群组描述",
|
||||
"e.g Group for developers": "例如:开发人员群组",
|
||||
"Edit": "编辑",
|
||||
"Delete group": "删除群组",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "您确定要删除这个群组吗?成员将失去对该群组可访问资源的访问权限。",
|
||||
"Delete": "删除",
|
||||
"Cancel": "取消",
|
||||
"Remove group member": "移除群组成员",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "您确定要从群组中移除这个用户吗?该用户将失去对该群组可访问资源的访问权限。",
|
||||
"Add group members": "添加群组成员",
|
||||
"Create": "创建",
|
||||
"User": "用户",
|
||||
"Status": "状态",
|
||||
"Active": "活跃",
|
||||
"Add members": "添加成员",
|
||||
"Search for users": "搜索用户",
|
||||
"No user found": "未找到用户"
|
||||
},
|
||||
"space": {
|
||||
"Spaces": "空间",
|
||||
"Create space": "创建空间",
|
||||
"Space": "空间",
|
||||
"Members": "成员",
|
||||
"Settings": "设置",
|
||||
"Details": "详情",
|
||||
"Name": "名称",
|
||||
"e.g Sales": "例如:销售",
|
||||
"Slug": "短链接",
|
||||
"Description": "描述",
|
||||
"e.g Space for sales team to collaborate": "例如:销售团队协作的空间",
|
||||
"Save": "保存",
|
||||
"Space name": "空间名称",
|
||||
"e.g Product Team": "例如:产品团队",
|
||||
"Space slug": "空间短链接",
|
||||
"e.g product": "例如:product",
|
||||
"Space description": "空间描述",
|
||||
"e.g Space for product team": "例如:产品团队的空间",
|
||||
"Create": "创建",
|
||||
"addSpaceMembers": "添加空间成员",
|
||||
"add": "添加",
|
||||
"selectRole": "选择角色",
|
||||
"Remove space member": "移除空间成员",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "您确定要从空间中移除这个用户吗?该用户将失去对这个空间的所有访问权限。",
|
||||
"Remove": "移除",
|
||||
"Cancel": "取消",
|
||||
"Member": "成员",
|
||||
"Role": "角色",
|
||||
"Failed to load page. An error occurred.": "页面加载失败。发生了一个错误。",
|
||||
"Add members": "添加成员",
|
||||
"Search for users and groups": "搜索用户和群组",
|
||||
"No user found": "未找到用户"
|
||||
}
|
||||
}
|
||||
}
|
||||
11
apps/client/public/locales/zh/setup-workspace.json
Normal file
11
apps/client/public/locales/zh/setup-workspace.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"Setup workspace": "设置工作空间",
|
||||
"Create workspace": "创建工作空间",
|
||||
"Workspace Name": "工作空间名称",
|
||||
"e.g ACME Inc": "例如:ACME Inc",
|
||||
"Your Name": "您的姓名",
|
||||
"enter your full name": "输入您的全名",
|
||||
"Your Email": "您的电子邮箱",
|
||||
"Password": "密码",
|
||||
"Enter a strong password": "输入一个强密码"
|
||||
}
|
||||
55
apps/client/public/locales/zh/translation.json
Normal file
55
apps/client/public/locales/zh/translation.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"common": {
|
||||
"Failed to fetch recent pages": "获取最近页面失败",
|
||||
"Untitled": "无标题",
|
||||
"No pages yet": "暂无页面",
|
||||
"Failed to load page. An error occurred.": "加载页面失败。发生错误。"
|
||||
},
|
||||
"layout": {
|
||||
"Home": "首页",
|
||||
"Workspace": "工作区",
|
||||
"Workspace settings": "工作区设置",
|
||||
"Manage members": "管理成员",
|
||||
"Account": "账户",
|
||||
"My profile": "我的个人资料",
|
||||
"My preferences": "我的偏好设置",
|
||||
"Logout": "退出登录",
|
||||
"Settings": "设置",
|
||||
"Profile": "个人资料",
|
||||
"Preferences": "偏好设置",
|
||||
"General": "常规",
|
||||
"Members": "成员",
|
||||
"Groups": "群组",
|
||||
"Spaces": "空间"
|
||||
},
|
||||
"home": {
|
||||
"Recently updated": "最近更新"
|
||||
},
|
||||
"space": {
|
||||
"Spaces you belong to": "您所属的空间",
|
||||
"Recently updated": "最近更新"
|
||||
},
|
||||
"page": {
|
||||
"Error fetching page data.": "获取页面数据时出错。",
|
||||
"untitled": "无标题",
|
||||
"Link copied": "链接已复制",
|
||||
"Copy link": "复制链接",
|
||||
"Full width": "全宽",
|
||||
"Page history": "页面历史",
|
||||
"Export": "导出",
|
||||
"Print PDF": "打印 PDF",
|
||||
"Delete": "删除"
|
||||
},
|
||||
"page-history": {
|
||||
"Page history": "页面历史",
|
||||
"Error loading page history.": "加载页面历史时出错。",
|
||||
"No page history saved yet.": "尚未保存页面历史。",
|
||||
"Please confirm your action": "请确认您的操作",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "您确定要恢复此版本吗?任何未版本化的更改将会丢失。",
|
||||
"Confirm": "确认",
|
||||
"Cancel": "取消",
|
||||
"Successfully restored": "恢复成功",
|
||||
"Restore": "恢复",
|
||||
"Error fetching page data.": "获取页面数据时出错。"
|
||||
}
|
||||
}
|
||||
59
apps/client/scripts/i18n-tools.js
Normal file
59
apps/client/scripts/i18n-tools.js
Normal file
@ -0,0 +1,59 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const localsPath = path.join(__dirname, "../public/locales");
|
||||
|
||||
const supportLanguages = ["en", "zh"];
|
||||
const supportCommands = ["add-ns"];
|
||||
|
||||
function ensureLanguageDirectories() {
|
||||
if (!fs.existsSync(localsPath)) {
|
||||
fs.mkdirSync(localsPath, { recursive: true });
|
||||
}
|
||||
|
||||
supportLanguages.forEach((lang) => {
|
||||
const langPath = path.join(localsPath, lang);
|
||||
|
||||
if (!fs.existsSync(langPath)) {
|
||||
fs.mkdirSync(langPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addNamespaces(namespaces) {
|
||||
supportLanguages.forEach((lang) => {
|
||||
const langPath = path.join(localsPath, lang);
|
||||
|
||||
namespaces.forEach((ns) => {
|
||||
const nsFilePath = path.join(langPath, `${ns}.json`);
|
||||
|
||||
if (!fs.existsSync(nsFilePath)) {
|
||||
fs.writeFileSync(nsFilePath, "{}", "utf8");
|
||||
console.log(`Created empty ${ns}.json file in ${lang} directory`);
|
||||
} else {
|
||||
console.log(
|
||||
`${ns}.json file already exists in ${lang} directory, skipping creation`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ensureLanguageDirectories();
|
||||
|
||||
const [command, ...params] = process.argv.slice(2);
|
||||
|
||||
if (!supportCommands.includes(command)) {
|
||||
console.warn(
|
||||
`Only support the follow commands: ${supportCommands.join(" ")} `,
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case "add-ns":
|
||||
addNamespaces(params);
|
||||
break;
|
||||
default:
|
||||
console.warn("You should input a command");
|
||||
}
|
||||
@ -14,7 +14,7 @@ import { useQuerySubscription } from "@/features/websocket/use-query-subscriptio
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { socketAtom } from "@/features/websocket/atoms/socket-atom.ts";
|
||||
import { useTreeSocket } from "@/features/websocket/use-tree-socket.ts";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useTransition } from "react";
|
||||
import { io } from "socket.io-client";
|
||||
import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom.ts";
|
||||
import { SOCKET_URL } from "@/features/websocket/types";
|
||||
@ -24,10 +24,14 @@ import PageRedirect from "@/pages/page/page-redirect.tsx";
|
||||
import Layout from "@/components/layouts/global/layout.tsx";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import InviteSignup from "@/pages/auth/invite-signup.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function App() {
|
||||
const [, setSocket] = useAtom(socketAtom);
|
||||
const authToken = useAtomValue(authTokensAtom);
|
||||
const { t } = useTranslation("translation", {
|
||||
keyPrefix: "common",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!authToken?.accessToken) {
|
||||
@ -74,7 +78,7 @@ export default function App() {
|
||||
path={"/s/:spaceSlug/p/:pageSlug"}
|
||||
element={
|
||||
<ErrorBoundary
|
||||
fallback={<>Failed to load page. An error occurred.</>}
|
||||
fallback={<>{t("Failed to load page. An error occurred.")}</>}
|
||||
>
|
||||
<Page />
|
||||
</ErrorBoundary>
|
||||
|
||||
@ -13,11 +13,14 @@ import { formattedDate } from "@/lib/time.ts";
|
||||
import { useRecentChangesQuery } from "@/features/page/queries/page-query.ts";
|
||||
import { IconFileDescription } from "@tabler/icons-react";
|
||||
import { getSpaceUrl } from "@/lib/config.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
spaceId?: string;
|
||||
}
|
||||
export default function RecentChanges({ spaceId }: Props) {
|
||||
const { t } = useTranslation("translation", { keyPrefix: "common" });
|
||||
|
||||
const { data: pages, isLoading, isError } = useRecentChangesQuery(spaceId);
|
||||
|
||||
if (isLoading) {
|
||||
@ -25,7 +28,7 @@ export default function RecentChanges({ spaceId }: Props) {
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <Text>Failed to fetch recent pages</Text>;
|
||||
return <Text>{t("Failed to fetch recent pages")}</Text>;
|
||||
}
|
||||
|
||||
return pages && pages.items.length > 0 ? (
|
||||
@ -43,7 +46,7 @@ export default function RecentChanges({ spaceId }: Props) {
|
||||
{page.icon || <IconFileDescription size={18} />}
|
||||
|
||||
<Text fw={500} size="md" lineClamp={1}>
|
||||
{page.title || "Untitled"}
|
||||
{page.title || t("Untitled")}
|
||||
</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
@ -73,7 +76,7 @@ export default function RecentChanges({ spaceId }: Props) {
|
||||
</ScrollArea>
|
||||
) : (
|
||||
<Text size="md" ta="center">
|
||||
No pages yet
|
||||
{t("No pages yet")}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@ -11,10 +11,14 @@ import {
|
||||
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
|
||||
import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const links = [{ link: APP_ROUTE.HOME, label: "Home" }];
|
||||
|
||||
export function AppHeader() {
|
||||
const { t } = useTranslation("translation", {
|
||||
keyPrefix: "layout",
|
||||
});
|
||||
const [mobileOpened] = useAtom(mobileSidebarAtom);
|
||||
const toggleMobile = useToggleSidebar(mobileSidebarAtom);
|
||||
|
||||
@ -25,7 +29,7 @@ export function AppHeader() {
|
||||
|
||||
const items = links.map((link) => (
|
||||
<Link key={link.label} to={link.link} className={classes.link}>
|
||||
{link.label}
|
||||
{t(link.label)}
|
||||
</Link>
|
||||
));
|
||||
|
||||
|
||||
@ -13,8 +13,12 @@ import { Link } from "react-router-dom";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
import useAuth from "@/features/auth/hooks/use-auth.ts";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function TopMenu() {
|
||||
const { t } = useTranslation("translation", {
|
||||
keyPrefix: "layout",
|
||||
});
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
const { logout } = useAuth();
|
||||
|
||||
@ -44,14 +48,14 @@ export default function TopMenu() {
|
||||
</UnstyledButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>Workspace</Menu.Label>
|
||||
<Menu.Label>{t("Workspace")}</Menu.Label>
|
||||
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
to={APP_ROUTE.SETTINGS.WORKSPACE.GENERAL}
|
||||
leftSection={<IconSettings size={16} />}
|
||||
>
|
||||
Workspace settings
|
||||
{t("Workspace settings")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
@ -59,12 +63,12 @@ export default function TopMenu() {
|
||||
to={APP_ROUTE.SETTINGS.WORKSPACE.MEMBERS}
|
||||
leftSection={<IconUsers size={16} />}
|
||||
>
|
||||
Manage members
|
||||
{t("Manage members")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Label>Account</Menu.Label>
|
||||
<Menu.Label>{t("Account")}</Menu.Label>
|
||||
<Menu.Item component={Link} to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<CustomAvatar
|
||||
@ -88,7 +92,7 @@ export default function TopMenu() {
|
||||
to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}
|
||||
leftSection={<IconUserCircle size={16} />}
|
||||
>
|
||||
My profile
|
||||
{t("My profile")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
@ -96,13 +100,13 @@ export default function TopMenu() {
|
||||
to={APP_ROUTE.SETTINGS.ACCOUNT.PREFERENCES}
|
||||
leftSection={<IconBrush size={16} />}
|
||||
>
|
||||
My preferences
|
||||
{t("My preferences")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item onClick={logout} leftSection={<IconLogout size={16} />}>
|
||||
Logout
|
||||
{t("Logout")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
} from "@tabler/icons-react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import classes from "./settings.module.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface DataItem {
|
||||
label: string;
|
||||
@ -51,6 +52,9 @@ const groupedData: DataGroup[] = [
|
||||
];
|
||||
|
||||
export default function SettingsSidebar() {
|
||||
const { t } = useTranslation("translation", {
|
||||
keyPrefix: "layout",
|
||||
});
|
||||
const location = useLocation();
|
||||
const [active, setActive] = useState(location.pathname);
|
||||
const navigate = useNavigate();
|
||||
@ -62,7 +66,7 @@ export default function SettingsSidebar() {
|
||||
const menuItems = groupedData.map((group) => (
|
||||
<div key={group.heading}>
|
||||
<Text c="dimmed" className={classes.linkHeader}>
|
||||
{group.heading}
|
||||
{t(group.heading)}
|
||||
</Text>
|
||||
{group.items.map((item) => (
|
||||
<Link
|
||||
@ -72,7 +76,7 @@ export default function SettingsSidebar() {
|
||||
to={item.path}
|
||||
>
|
||||
<item.icon className={classes.linkIcon} stroke={2} />
|
||||
<span>{item.label}</span>
|
||||
<span>{t(item.label)}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
@ -89,7 +93,7 @@ export default function SettingsSidebar() {
|
||||
>
|
||||
<IconArrowLeft stroke={2} />
|
||||
</ActionIcon>
|
||||
<Text fw={500}>Settings</Text>
|
||||
<Text fw={500}>{t("Settings")}</Text>
|
||||
</Group>
|
||||
|
||||
<ScrollArea w="100%">{menuItems}</ScrollArea>
|
||||
|
||||
@ -17,6 +17,7 @@ import useAuth from "@/features/auth/hooks/use-auth";
|
||||
import classes from "@/features/auth/components/auth.module.css";
|
||||
import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-query.ts";
|
||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2),
|
||||
@ -26,6 +27,7 @@ const formSchema = z.object({
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
export function InviteSignUpForm() {
|
||||
const { t } = useTranslation("invite-signup");
|
||||
const params = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
@ -55,7 +57,7 @@ export function InviteSignUpForm() {
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <div>invalid invitation link</div>;
|
||||
return <div>{t("invalid invitation link")}</div>;
|
||||
}
|
||||
|
||||
if (!invitation) {
|
||||
@ -66,7 +68,7 @@ export function InviteSignUpForm() {
|
||||
<Container size={420} my={40} className={classes.container}>
|
||||
<Box p="xl" mt={200}>
|
||||
<Title order={2} ta="center" fw={500} mb="md">
|
||||
Join the workspace
|
||||
{t("Join the workspace")}
|
||||
</Title>
|
||||
|
||||
<Stack align="stretch" justify="center" gap="xl">
|
||||
@ -74,8 +76,8 @@ export function InviteSignUpForm() {
|
||||
<TextInput
|
||||
id="name"
|
||||
type="text"
|
||||
label="Name"
|
||||
placeholder="enter your full name"
|
||||
label={t("Name")}
|
||||
placeholder={t("enter your full name")}
|
||||
variant="filled"
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
@ -83,7 +85,7 @@ export function InviteSignUpForm() {
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
label={t("Email")}
|
||||
value={invitation.email}
|
||||
disabled
|
||||
variant="filled"
|
||||
@ -91,14 +93,14 @@ export function InviteSignUpForm() {
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Your password"
|
||||
label={t("Password")}
|
||||
placeholder={t("Your password")}
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||
Sign Up
|
||||
{t("Sign Up")}
|
||||
</Button>
|
||||
</form>
|
||||
</Stack>
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import classes from "./auth.module.css";
|
||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z
|
||||
@ -25,6 +26,7 @@ const formSchema = z.object({
|
||||
export function LoginForm() {
|
||||
const { signIn, isLoading } = useAuth();
|
||||
useRedirectIfAuthenticated();
|
||||
const { t } = useTranslation("login");
|
||||
|
||||
const form = useForm<ILogin>({
|
||||
validate: zodResolver(formSchema),
|
||||
@ -42,28 +44,28 @@ export function LoginForm() {
|
||||
<Container size={420} my={40} className={classes.container}>
|
||||
<Box p="xl" mt={200}>
|
||||
<Title order={2} ta="center" fw={500} mb="md">
|
||||
Login
|
||||
{t("Login")}
|
||||
</Title>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
label={t("Email")}
|
||||
placeholder="email@example.com"
|
||||
variant="filled"
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Your password"
|
||||
label={t("Password")}
|
||||
placeholder={t("Your password")}
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||
Sign In
|
||||
{t("Sign In")}
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
import { ISetupWorkspace } from "@/features/auth/types/auth.types";
|
||||
import useAuth from "@/features/auth/hooks/use-auth";
|
||||
import classes from "@/features/auth/components/auth.module.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
workspaceName: z.string().min(2).max(60),
|
||||
@ -28,6 +29,7 @@ export function SetupWorkspaceForm() {
|
||||
const { setupWorkspace, isLoading } = useAuth();
|
||||
// useRedirectIfAuthenticated();
|
||||
|
||||
const { t } = useTranslation("setup-workspace");
|
||||
const form = useForm<ISetupWorkspace>({
|
||||
validate: zodResolver(formSchema),
|
||||
initialValues: {
|
||||
@ -46,15 +48,15 @@ export function SetupWorkspaceForm() {
|
||||
<Container size={420} my={40} className={classes.container}>
|
||||
<Box p="xl" mt={200}>
|
||||
<Title order={2} ta="center" fw={500} mb="md">
|
||||
Create workspace
|
||||
{t("Create workspace")}
|
||||
</Title>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<TextInput
|
||||
id="workspaceName"
|
||||
type="text"
|
||||
label="Workspace Name"
|
||||
placeholder="e.g ACME Inc"
|
||||
label={t("Workspace Name")}
|
||||
placeholder={t("e.g ACME Inc")}
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("workspaceName")}
|
||||
@ -63,8 +65,8 @@ export function SetupWorkspaceForm() {
|
||||
<TextInput
|
||||
id="name"
|
||||
type="text"
|
||||
label="Your Name"
|
||||
placeholder="enter your full name"
|
||||
label={t("Your Name")}
|
||||
placeholder={t("enter your full name")}
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("name")}
|
||||
@ -73,7 +75,7 @@ export function SetupWorkspaceForm() {
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
label="Your Email"
|
||||
label={t("Your Email")}
|
||||
placeholder="email@example.com"
|
||||
variant="filled"
|
||||
mt="md"
|
||||
@ -81,14 +83,14 @@ export function SetupWorkspaceForm() {
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Enter a strong password"
|
||||
label={t("Password")}
|
||||
placeholder={t("Enter a strong password")}
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||
Setup workspace
|
||||
{t("Setup workspace")}
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
|
||||
@ -4,8 +4,12 @@ import React, { useState } from "react";
|
||||
import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useAddGroupMemberMutation } from "@/features/group/queries/group-query.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function AddGroupMemberModal() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.group",
|
||||
});
|
||||
const { groupId } = useParams();
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [userIds, setUserIds] = useState<string[]>([]);
|
||||
@ -27,19 +31,19 @@ export default function AddGroupMemberModal() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={open}>Add group members</Button>
|
||||
<Button onClick={open}>{t("addGroupMembers")}</Button>
|
||||
|
||||
<Modal opened={opened} onClose={close} title="Add group members">
|
||||
<Modal opened={opened} onClose={close} title={t("addGroupMembers")}>
|
||||
<Divider size="xs" mb="xs" />
|
||||
|
||||
<MultiUserSelect
|
||||
label={"Add group members"}
|
||||
label={t("addGroupMembers")}
|
||||
onChange={handleMultiSelectChange}
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button onClick={handleSubmit} type="submit">
|
||||
Add
|
||||
{t("add")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Modal>
|
||||
|
||||
@ -5,6 +5,7 @@ import { useForm, zodResolver } from "@mantine/form";
|
||||
import * as z from "zod";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2).max(50),
|
||||
@ -14,6 +15,9 @@ const formSchema = z.object({
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
export function CreateGroupForm() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.group",
|
||||
});
|
||||
const createGroupMutation = useCreateGroupMutation();
|
||||
const [userIds, setUserIds] = useState<string[]>([]);
|
||||
const navigate = useNavigate();
|
||||
@ -52,16 +56,16 @@ export function CreateGroupForm() {
|
||||
<TextInput
|
||||
withAsterisk
|
||||
id="name"
|
||||
label="Group name"
|
||||
placeholder="e.g Developers"
|
||||
label={t("Group name")}
|
||||
placeholder={t("e.g Developers")}
|
||||
variant="filled"
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
id="description"
|
||||
label="Group description"
|
||||
placeholder="e.g Group for developers"
|
||||
label={t("Group description")}
|
||||
placeholder={t("e.g Group for developers")}
|
||||
variant="filled"
|
||||
autosize
|
||||
minRows={2}
|
||||
@ -70,13 +74,13 @@ export function CreateGroupForm() {
|
||||
/>
|
||||
|
||||
<MultiUserSelect
|
||||
label={"Add group members"}
|
||||
label={t("Add group members")}
|
||||
onChange={handleMultiSelectChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button type="submit">Create</Button>
|
||||
<Button type="submit">{t("Create")}</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Box>
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
import { Button, Divider, Modal } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { CreateGroupForm } from "@/features/group/components/create-group-form.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function CreateGroupModal() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.group",
|
||||
});
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={open}>Create group</Button>
|
||||
<Button onClick={open}>{t("Create group")}</Button>
|
||||
|
||||
<Modal opened={opened} onClose={close} title="Create group">
|
||||
<Modal opened={opened} onClose={close} title={t("Create group")}>
|
||||
<Divider size="xs" mb="xs" />
|
||||
<CreateGroupForm />
|
||||
</Modal>
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import * as z from "zod";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2).max(50),
|
||||
@ -18,6 +19,9 @@ interface EditGroupFormProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
export function EditGroupForm({ onClose }: EditGroupFormProps) {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.group",
|
||||
});
|
||||
const updateGroupMutation = useUpdateGroupMutation();
|
||||
const { isSuccess } = updateGroupMutation;
|
||||
const { groupId } = useParams();
|
||||
@ -60,16 +64,16 @@ export function EditGroupForm({ onClose }: EditGroupFormProps) {
|
||||
<TextInput
|
||||
withAsterisk
|
||||
id="name"
|
||||
label="Group name"
|
||||
placeholder="e.g Developers"
|
||||
label={t("Group name")}
|
||||
placeholder={t("e.g Developers")}
|
||||
variant="filled"
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
id="description"
|
||||
label="Group description"
|
||||
placeholder="e.g Group for developers"
|
||||
label={t("Group description")}
|
||||
placeholder={t("e.g Group for developers")}
|
||||
variant="filled"
|
||||
autosize
|
||||
minRows={2}
|
||||
@ -79,7 +83,7 @@ export function EditGroupForm({ onClose }: EditGroupFormProps) {
|
||||
</Stack>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button type="submit">Edit</Button>
|
||||
<Button type="submit">{t("Edit")}</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Box>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Divider, Modal } from "@mantine/core";
|
||||
import { EditGroupForm } from "@/features/group/components/edit-group-form.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface EditGroupModalProps {
|
||||
opened: boolean;
|
||||
@ -10,9 +11,13 @@ export default function EditGroupModal({
|
||||
opened,
|
||||
onClose,
|
||||
}: EditGroupModalProps) {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.group",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal opened={opened} onClose={onClose} title="Edit group">
|
||||
<Modal opened={opened} onClose={onClose} title={t("Edit group")}>
|
||||
<Divider size="xs" mb="xs" />
|
||||
<EditGroupForm onClose={onClose} />
|
||||
</Modal>
|
||||
|
||||
@ -9,8 +9,12 @@ import { IconDots, IconTrash } from "@tabler/icons-react";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import EditGroupModal from "@/features/group/components/edit-group-modal.tsx";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function GroupActionMenu() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.group",
|
||||
});
|
||||
const { groupId } = useParams();
|
||||
const { data: group, isLoading } = useGroupQuery(groupId);
|
||||
const deleteGroupMutation = useDeleteGroupMutation();
|
||||
@ -24,15 +28,16 @@ export default function GroupActionMenu() {
|
||||
|
||||
const openDeleteModal = () =>
|
||||
modals.openConfirmModal({
|
||||
title: "Delete group",
|
||||
title: t("Delete group"),
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Are you sure you want to delete this group? Members will lose access
|
||||
to resources this group has access to.
|
||||
{t(
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.",
|
||||
)}
|
||||
</Text>
|
||||
),
|
||||
centered: true,
|
||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
||||
labels: { confirm: t("Delete"), cancel: t("Cancel") },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: onDelete,
|
||||
});
|
||||
@ -57,7 +62,7 @@ export default function GroupActionMenu() {
|
||||
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item onClick={open} disabled={group.isDefault}>
|
||||
Edit group
|
||||
{t("Edit group")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
@ -66,7 +71,7 @@ export default function GroupActionMenu() {
|
||||
disabled={group.isDefault}
|
||||
leftSection={<IconTrash size={16} stroke={2} />}
|
||||
>
|
||||
Delete group
|
||||
{t("Delete group")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@ -7,6 +7,7 @@ import { useDisclosure } from "@mantine/hooks";
|
||||
import EditGroupModal from "@/features/group/components/edit-group-modal.tsx";
|
||||
import GroupActionMenu from "@/features/group/components/group-action-menu.tsx";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function GroupDetails() {
|
||||
const { groupId } = useParams();
|
||||
|
||||
@ -3,8 +3,12 @@ import { useGetGroupsQuery } from "@/features/group/queries/group-query";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function GroupList() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.group",
|
||||
});
|
||||
const { data, isLoading } = useGetGroupsQuery();
|
||||
|
||||
return (
|
||||
@ -13,8 +17,8 @@ export default function GroupList() {
|
||||
<Table highlightOnHover verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Group</Table.Th>
|
||||
<Table.Th>Members</Table.Th>
|
||||
<Table.Th>{t("Group")}</Table.Th>
|
||||
<Table.Th>{t("Members")}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
|
||||
@ -9,8 +9,12 @@ import { IconDots } from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function GroupMembersList() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.group",
|
||||
});
|
||||
const { groupId } = useParams();
|
||||
const { data, isLoading } = useGroupMembersQuery(groupId);
|
||||
const removeGroupMember = useRemoveGroupMemberMutation();
|
||||
@ -26,15 +30,16 @@ export default function GroupMembersList() {
|
||||
|
||||
const openRemoveModal = (userId: string) =>
|
||||
modals.openConfirmModal({
|
||||
title: "Remove group member",
|
||||
title: t("Remove group member"),
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Are you sure you want to remove this user from the group? The user
|
||||
will lose access to resources this group has access to.
|
||||
{t(
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.",
|
||||
)}
|
||||
</Text>
|
||||
),
|
||||
centered: true,
|
||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
||||
labels: { confirm: t("Delete"), cancel: t("Cancel") },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => onRemove(userId),
|
||||
});
|
||||
@ -45,8 +50,8 @@ export default function GroupMembersList() {
|
||||
<Table verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>User</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>{t("User")}</Table.Th>
|
||||
<Table.Th>{t("Status")}</Table.Th>
|
||||
<Table.Th></Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
@ -69,7 +74,7 @@ export default function GroupMembersList() {
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<Badge variant="light">Active</Badge>
|
||||
<Badge variant="light">{t("Active")}</Badge>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
@ -90,7 +95,7 @@ export default function GroupMembersList() {
|
||||
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item onClick={() => openRemoveModal(user.id)}>
|
||||
Remove group member
|
||||
{t("Remove group member")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@ -4,6 +4,7 @@ import { useWorkspaceMembersQuery } from "@/features/workspace/queries/workspace
|
||||
import { IUser } from "@/features/user/types/user.types.ts";
|
||||
import { Group, MultiSelect, MultiSelectProps, Text } from "@mantine/core";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface MultiUserSelectProps {
|
||||
onChange: (value: string[]) => void;
|
||||
@ -29,6 +30,9 @@ const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
|
||||
);
|
||||
|
||||
export function MultiUserSelect({ onChange, label }: MultiUserSelectProps) {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.group",
|
||||
});
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [debouncedQuery] = useDebouncedValue(searchValue, 500);
|
||||
const { data: users, isLoading } = useWorkspaceMembersQuery({
|
||||
@ -65,15 +69,15 @@ export function MultiUserSelect({ onChange, label }: MultiUserSelectProps) {
|
||||
renderOption={renderMultiSelectOption}
|
||||
hidePickedOptions
|
||||
maxDropdownHeight={300}
|
||||
label={label || "Add members"}
|
||||
placeholder="Search for users"
|
||||
label={label || t("Add members")}
|
||||
placeholder={t("Search for users")}
|
||||
searchable
|
||||
searchValue={searchValue}
|
||||
onSearchChange={setSearchValue}
|
||||
clearable
|
||||
variant="filled"
|
||||
onChange={onChange}
|
||||
nothingFoundMessage="No user found"
|
||||
nothingFoundMessage={t("No user found")}
|
||||
maxValues={50}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
import { Text, Tabs, Space } from "@mantine/core";
|
||||
import { IconClockHour3 } from "@tabler/icons-react";
|
||||
import RecentChanges from "@/components/common/recent-changes.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function HomeTabs() {
|
||||
const { t } = useTranslation("translation", { keyPrefix: "home" });
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="recent">
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="recent" leftSection={<IconClockHour3 size={18} />}>
|
||||
<Text size="sm" fw={500}>
|
||||
Recently updated
|
||||
{t("Recently updated")}
|
||||
</Text>
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
@ -16,12 +16,15 @@ import {
|
||||
} from "@/features/editor/atoms/editor-atoms";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
pageId: string;
|
||||
}
|
||||
|
||||
function HistoryList({ pageId }: Props) {
|
||||
const { t } = useTranslation("translation", { keyPrefix: "pageHistory" });
|
||||
|
||||
const [activeHistoryId, setActiveHistoryId] = useAtom(activeHistoryIdAtom);
|
||||
const {
|
||||
data: pageHistoryList,
|
||||
@ -36,14 +39,15 @@ function HistoryList({ pageId }: Props) {
|
||||
|
||||
const confirmModal = () =>
|
||||
modals.openConfirmModal({
|
||||
title: "Please confirm your action",
|
||||
title: t("Please confirm your action"),
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Are you sure you want to restore this version? Any changes not
|
||||
versioned will be lost.
|
||||
{t(
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.",
|
||||
)}
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: "Confirm", cancel: "Cancel" },
|
||||
labels: { confirm: t("Confirm"), cancel: t("Cancel") },
|
||||
onConfirm: handleRestore,
|
||||
});
|
||||
|
||||
@ -60,7 +64,7 @@ function HistoryList({ pageId }: Props) {
|
||||
.setContent(activeHistoryData.content)
|
||||
.run();
|
||||
setHistoryModalOpen(false);
|
||||
notifications.show({ message: "Successfully restored" });
|
||||
notifications.show({ message: t("Successfully restored") });
|
||||
}
|
||||
}, [activeHistoryData]);
|
||||
|
||||
@ -79,11 +83,11 @@ function HistoryList({ pageId }: Props) {
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <div>Error loading page history.</div>;
|
||||
return <div>{t("Error loading page history.")}</div>;
|
||||
}
|
||||
|
||||
if (!pageHistoryList || pageHistoryList.items.length === 0) {
|
||||
return <>No page history saved yet.</>;
|
||||
return <>{t("No page history saved yet.")}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -104,14 +108,14 @@ function HistoryList({ pageId }: Props) {
|
||||
|
||||
<Group p="xs" wrap="nowrap">
|
||||
<Button size="compact-md" onClick={confirmModal}>
|
||||
Restore
|
||||
{t("Restore")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="compact-md"
|
||||
onClick={() => setHistoryModalOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
@ -2,11 +2,13 @@ import { Modal, Text } from "@mantine/core";
|
||||
import { useAtom } from "jotai";
|
||||
import { historyAtoms } from "@/features/page-history/atoms/history-atoms";
|
||||
import HistoryModalBody from "@/features/page-history/components/history-modal-body";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
pageId: string;
|
||||
}
|
||||
export default function HistoryModal({ pageId }: Props) {
|
||||
const { t } = useTranslation("translation", { keyPrefix: "pageHistory" });
|
||||
const [isModalOpen, setModalOpen] = useAtom(historyAtoms);
|
||||
|
||||
return (
|
||||
@ -21,7 +23,7 @@ export default function HistoryModal({ pageId }: Props) {
|
||||
<Modal.Header>
|
||||
<Modal.Title>
|
||||
<Text size="md" fw={500}>
|
||||
Page history
|
||||
{t("Page history")}
|
||||
</Text>
|
||||
</Modal.Title>
|
||||
<Modal.CloseButton />
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { usePageHistoryQuery } from '@/features/page-history/queries/page-history-query';
|
||||
import { HistoryEditor } from '@/features/page-history/components/history-editor';
|
||||
import { usePageHistoryQuery } from "@/features/page-history/queries/page-history-query";
|
||||
import { HistoryEditor } from "@/features/page-history/components/history-editor";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface HistoryProps {
|
||||
historyId: string;
|
||||
}
|
||||
|
||||
function HistoryView({ historyId }: HistoryProps) {
|
||||
const { t } = useTranslation("translation", { keyPrefix: "pageHistory" });
|
||||
const { data, isLoading, isError } = usePageHistoryQuery(historyId);
|
||||
|
||||
if (isLoading) {
|
||||
@ -13,13 +15,15 @@ function HistoryView({ historyId }: HistoryProps) {
|
||||
}
|
||||
|
||||
if (isError || !data) {
|
||||
return <div>Error fetching page data.</div>;
|
||||
return <div>{t("Error fetching page data.")}</div>;
|
||||
}
|
||||
|
||||
return (data &&
|
||||
return (
|
||||
data && (
|
||||
<div>
|
||||
<HistoryEditor content={data.content} title={data.title} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
|
||||
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
|
||||
import { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx";
|
||||
import PageExportModal from "@/features/page/components/page-export-modal.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface PageHeaderMenuProps {
|
||||
readOnly?: boolean;
|
||||
@ -52,6 +53,8 @@ interface PageActionMenuProps {
|
||||
readOnly?: boolean;
|
||||
}
|
||||
function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
const { t } = useTranslation("translation", { keyPrefix: "page" });
|
||||
|
||||
const [, setHistoryModalOpen] = useAtom(historyAtoms);
|
||||
const clipboard = useClipboard({ timeout: 500 });
|
||||
const { pageSlug, spaceSlug } = useParams();
|
||||
@ -68,7 +71,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
getAppUrl() + buildPageUrl(spaceSlug, page.slugId, page.title);
|
||||
|
||||
clipboard.copy(pageUrl);
|
||||
notifications.show({ message: "Link copied" });
|
||||
notifications.show({ message: t("Link copied") });
|
||||
};
|
||||
|
||||
const handlePrint = () => {
|
||||
@ -106,13 +109,13 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
leftSection={<IconLink size={16} />}
|
||||
onClick={handleCopyLink}
|
||||
>
|
||||
Copy link
|
||||
{t("Copy link")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item leftSection={<IconArrowsHorizontal size={16} />}>
|
||||
<Group wrap="nowrap">
|
||||
<PageWidthToggle label="Full width" />
|
||||
<PageWidthToggle label={t("Full width")} />
|
||||
</Group>
|
||||
</Menu.Item>
|
||||
|
||||
@ -120,7 +123,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
leftSection={<IconHistory size={16} />}
|
||||
onClick={openHistoryModal}
|
||||
>
|
||||
Page history
|
||||
{t("Page history")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Divider />
|
||||
@ -129,14 +132,14 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
leftSection={<IconDownload size={16} />}
|
||||
onClick={openExportModal}
|
||||
>
|
||||
Export
|
||||
{t("Export")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
leftSection={<IconPrinter size={16} />}
|
||||
onClick={handlePrint}
|
||||
>
|
||||
Print PDF
|
||||
{t("Print PDF")}
|
||||
</Menu.Item>
|
||||
|
||||
{!readOnly && (
|
||||
@ -147,7 +150,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
leftSection={<IconTrash size={16} />}
|
||||
onClick={handleDeletePage}
|
||||
>
|
||||
Delete
|
||||
{t("Delete")}
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -5,6 +5,7 @@ import { useAddSpaceMemberMutation } from "@/features/space/queries/space-query.
|
||||
import { MultiMemberSelect } from "@/features/space/components/multi-member-select.tsx";
|
||||
import { SpaceMemberRole } from "@/features/space/components/space-member-role.tsx";
|
||||
import { SpaceRole } from "@/lib/types.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface AddSpaceMemberModalProps {
|
||||
spaceId: string;
|
||||
@ -12,6 +13,9 @@ interface AddSpaceMemberModalProps {
|
||||
export default function AddSpaceMembersModal({
|
||||
spaceId,
|
||||
}: AddSpaceMemberModalProps) {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.space",
|
||||
});
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [memberIds, setMemberIds] = useState<string[]>([]);
|
||||
const [role, setRole] = useState<string>(SpaceRole.WRITER);
|
||||
@ -48,8 +52,8 @@ export default function AddSpaceMembersModal({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={open}>Add space members</Button>
|
||||
<Modal opened={opened} onClose={close} title="Add space members">
|
||||
<Button onClick={open}>{t("addSpaceMembers")}</Button>
|
||||
<Modal opened={opened} onClose={close} title={t("addSpaceMembers")}>
|
||||
<Divider size="xs" mb="xs" />
|
||||
|
||||
<Stack>
|
||||
@ -57,13 +61,13 @@ export default function AddSpaceMembersModal({
|
||||
<SpaceMemberRole
|
||||
onSelect={handleRoleSelection}
|
||||
defaultRole={role}
|
||||
label="Select role"
|
||||
label={t("selectRole")}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button onClick={handleSubmit} type="submit">
|
||||
Add
|
||||
{t("add")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Modal>
|
||||
|
||||
@ -6,6 +6,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { useCreateSpaceMutation } from "@/features/space/queries/space-query.ts";
|
||||
import { computeSpaceSlug } from "@/lib";
|
||||
import { getSpaceUrl } from "@/lib/config.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2).max(50),
|
||||
@ -22,6 +23,9 @@ const formSchema = z.object({
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
export function CreateSpaceForm() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.space",
|
||||
});
|
||||
const createSpaceMutation = useCreateSpaceMutation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -73,8 +77,8 @@ export function CreateSpaceForm() {
|
||||
<TextInput
|
||||
withAsterisk
|
||||
id="name"
|
||||
label="Space name"
|
||||
placeholder="e.g Product Team"
|
||||
label={t("Space name")}
|
||||
placeholder={t("e.g Product Team")}
|
||||
variant="filled"
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
@ -82,16 +86,16 @@ export function CreateSpaceForm() {
|
||||
<TextInput
|
||||
withAsterisk
|
||||
id="slug"
|
||||
label="Space slug"
|
||||
placeholder="e.g product"
|
||||
label={t("Space slug")}
|
||||
placeholder={t("e.g product")}
|
||||
variant="filled"
|
||||
{...form.getInputProps("slug")}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
id="description"
|
||||
label="Space description"
|
||||
placeholder="e.g Space for product team"
|
||||
label={t("Space description")}
|
||||
placeholder={t("e.g Space for product team")}
|
||||
variant="filled"
|
||||
autosize
|
||||
minRows={2}
|
||||
@ -101,7 +105,7 @@ export function CreateSpaceForm() {
|
||||
</Stack>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button type="submit">Create</Button>
|
||||
<Button type="submit">{t("Create")}</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Box>
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
import { Button, Divider, Modal } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { CreateSpaceForm } from "@/features/space/components/create-space-form.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function CreateSpaceModal() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.space",
|
||||
});
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={open}>Create space</Button>
|
||||
<Button onClick={open}>{t("Create space")}</Button>
|
||||
|
||||
<Modal opened={opened} onClose={close} title="Create space">
|
||||
<Modal opened={opened} onClose={close} title={t("Create space")}>
|
||||
<Divider size="xs" mb="xs" />
|
||||
<CreateSpaceForm />
|
||||
</Modal>
|
||||
|
||||
@ -4,6 +4,7 @@ import { useForm, zodResolver } from "@mantine/form";
|
||||
import * as z from "zod";
|
||||
import { useUpdateSpaceMutation } from "@/features/space/queries/space-query.ts";
|
||||
import { ISpace } from "@/features/space/types/space.types.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2).max(50),
|
||||
@ -16,6 +17,9 @@ interface EditSpaceFormProps {
|
||||
readOnly?: boolean;
|
||||
}
|
||||
export function EditSpaceForm({ space, readOnly }: EditSpaceFormProps) {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.space",
|
||||
});
|
||||
const updateSpaceMutation = useUpdateSpaceMutation();
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
@ -51,8 +55,8 @@ export function EditSpaceForm({ space, readOnly }: EditSpaceFormProps) {
|
||||
<Stack>
|
||||
<TextInput
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="e.g Sales"
|
||||
label={t("Name")}
|
||||
placeholder={t("e.g Sales")}
|
||||
variant="filled"
|
||||
readOnly={readOnly}
|
||||
{...form.getInputProps("name")}
|
||||
@ -60,7 +64,7 @@ export function EditSpaceForm({ space, readOnly }: EditSpaceFormProps) {
|
||||
|
||||
<TextInput
|
||||
id="slug"
|
||||
label="Slug"
|
||||
label={t("Slug")}
|
||||
variant="filled"
|
||||
readOnly
|
||||
value={space.slug}
|
||||
@ -68,8 +72,8 @@ export function EditSpaceForm({ space, readOnly }: EditSpaceFormProps) {
|
||||
|
||||
<Textarea
|
||||
id="description"
|
||||
label="Description"
|
||||
placeholder="e.g Space for sales team to collaborate"
|
||||
label={t("Description")}
|
||||
placeholder={t("e.g Space for sales team to collaborate")}
|
||||
variant="filled"
|
||||
readOnly={readOnly}
|
||||
autosize
|
||||
@ -82,7 +86,7 @@ export function EditSpaceForm({ space, readOnly }: EditSpaceFormProps) {
|
||||
{!readOnly && (
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button type="submit" disabled={!form.isDirty()}>
|
||||
Save
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
@ -6,6 +6,7 @@ import { useSearchSuggestionsQuery } from "@/features/search/queries/search-quer
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import { IUser } from "@/features/user/types/user.types.ts";
|
||||
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface MultiMemberSelectProps {
|
||||
onChange: (value: string[]) => void;
|
||||
@ -30,6 +31,9 @@ const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
|
||||
);
|
||||
|
||||
export function MultiMemberSelect({ onChange }: MultiMemberSelectProps) {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.space",
|
||||
});
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [debouncedQuery] = useDebouncedValue(searchValue, 500);
|
||||
const { data: suggestion, isLoading } = useSearchSuggestionsQuery({
|
||||
@ -103,8 +107,8 @@ export function MultiMemberSelect({ onChange }: MultiMemberSelectProps) {
|
||||
renderOption={renderMultiSelectOption}
|
||||
hidePickedOptions
|
||||
maxDropdownHeight={300}
|
||||
label="Add members"
|
||||
placeholder="Search for users and groups"
|
||||
label={t("Add members")}
|
||||
placeholder={t("Search for users and groups")}
|
||||
searchable
|
||||
searchValue={searchValue}
|
||||
onSearchChange={setSearchValue}
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
SpaceCaslAction,
|
||||
SpaceCaslSubject,
|
||||
} from "@/features/space/permissions/permissions.type.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface SpaceSettingsModalProps {
|
||||
spaceId: string;
|
||||
@ -21,6 +22,9 @@ export default function SpaceSettingsModal({
|
||||
opened,
|
||||
onClose,
|
||||
}: SpaceSettingsModalProps) {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.space",
|
||||
});
|
||||
const { data: space, isLoading } = useSpaceQuery(spaceId);
|
||||
|
||||
const spaceRules = space?.membership?.permissions;
|
||||
@ -48,10 +52,10 @@ export default function SpaceSettingsModal({
|
||||
<Tabs defaultValue="members">
|
||||
<Tabs.List>
|
||||
<Tabs.Tab fw={500} value="general">
|
||||
Settings
|
||||
{t("Settings")}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab fw={500} value="members">
|
||||
Members
|
||||
{t("Members")}
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
|
||||
@ -2,12 +2,16 @@ import React from "react";
|
||||
import { useSpaceQuery } from "@/features/space/queries/space-query.ts";
|
||||
import { EditSpaceForm } from "@/features/space/components/edit-space-form.tsx";
|
||||
import { Text } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface SpaceDetailsProps {
|
||||
spaceId: string;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.space",
|
||||
});
|
||||
const { data: space, isLoading } = useSpaceQuery(spaceId);
|
||||
|
||||
return (
|
||||
@ -15,7 +19,7 @@ export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
||||
{space && (
|
||||
<div>
|
||||
<Text my="md" fw={600}>
|
||||
Details
|
||||
{t("Details")}
|
||||
</Text>
|
||||
<EditSpaceForm space={space} readOnly={readOnly} />
|
||||
</div>
|
||||
|
||||
@ -5,8 +5,11 @@ import { getSpaceUrl } from "@/lib/config.ts";
|
||||
import { Link } from "react-router-dom";
|
||||
import classes from "./space-grid.module.css";
|
||||
import { formatMemberCount } from "@/lib";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function SpaceGrid() {
|
||||
const { t } = useTranslation("translation", { keyPrefix: "space" });
|
||||
|
||||
const { data, isLoading } = useGetSpacesQuery();
|
||||
|
||||
const cards = data?.items.map((space, index) => (
|
||||
@ -41,7 +44,7 @@ export default function SpaceGrid() {
|
||||
return (
|
||||
<>
|
||||
<Text fz="sm" fw={500} mb={"md"}>
|
||||
Spaces you belong to
|
||||
{t("Spaces you belong to")}
|
||||
</Text>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, xs: 2, sm: 3 }}>{cards}</SimpleGrid>
|
||||
|
||||
@ -3,8 +3,12 @@ import { IconClockHour3 } from "@tabler/icons-react";
|
||||
import RecentChanges from "@/components/common/recent-changes.tsx";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function SpaceHomeTabs() {
|
||||
const { t } = useTranslation("translaction", {
|
||||
keyPrefix: "workspace.space",
|
||||
});
|
||||
const { spaceSlug } = useParams();
|
||||
const { data: space } = useGetSpaceBySlugQuery(spaceSlug);
|
||||
|
||||
@ -13,7 +17,7 @@ export default function SpaceHomeTabs() {
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="recent" leftSection={<IconClockHour3 size={18} />}>
|
||||
<Text size="sm" fw={500}>
|
||||
Recently updated
|
||||
{t("Recently updated")}
|
||||
</Text>
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
@ -4,8 +4,12 @@ import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts";
|
||||
import SpaceSettingsModal from "@/features/space/components/settings-modal.tsx";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { formatMemberCount } from "@/lib";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function SpaceList() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.space",
|
||||
});
|
||||
const { data, isLoading } = useGetSpacesQuery();
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [selectedSpaceId, setSelectedSpaceId] = useState<string>(null);
|
||||
@ -21,8 +25,8 @@ export default function SpaceList() {
|
||||
<Table highlightOnHover verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Space</Table.Th>
|
||||
<Table.Th>Members</Table.Th>
|
||||
<Table.Th>{t("Space")}</Table.Th>
|
||||
<Table.Th>{t("Members")}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
spaceRoleData,
|
||||
} from "@/features/space/types/space-role-data.ts";
|
||||
import { formatMemberCount } from "@/lib";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type MemberType = "user" | "group";
|
||||
interface SpaceMembersProps {
|
||||
@ -26,6 +27,9 @@ export default function SpaceMembersList({
|
||||
spaceId,
|
||||
readOnly,
|
||||
}: SpaceMembersProps) {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.space",
|
||||
});
|
||||
const { data, isLoading } = useSpaceMembersQuery(spaceId);
|
||||
const removeSpaceMember = useRemoveSpaceMemberMutation();
|
||||
const changeSpaceMemberRoleMutation = useChangeSpaceMemberRoleMutation();
|
||||
@ -77,15 +81,16 @@ export default function SpaceMembersList({
|
||||
|
||||
const openRemoveModal = (memberId: string, type: MemberType) =>
|
||||
modals.openConfirmModal({
|
||||
title: "Remove space member",
|
||||
title: t("Remove space member"),
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Are you sure you want to remove this user from the space? The user
|
||||
will lose all access to this space.
|
||||
{t(
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.",
|
||||
)}
|
||||
</Text>
|
||||
),
|
||||
centered: true,
|
||||
labels: { confirm: "Remove", cancel: "Cancel" },
|
||||
labels: { confirm: t("Remove"), cancel: t("Cancel") },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => onRemove(memberId, type),
|
||||
});
|
||||
@ -96,8 +101,8 @@ export default function SpaceMembersList({
|
||||
<Table verticalSpacing={8}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Member</Table.Th>
|
||||
<Table.Th>Role</Table.Th>
|
||||
<Table.Th>{t("Member")}</Table.Th>
|
||||
<Table.Th>{t("Role")}</Table.Th>
|
||||
<Table.Th></Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
@ -168,7 +173,7 @@ export default function SpaceMembersList({
|
||||
openRemoveModal(member.id, member.type)
|
||||
}
|
||||
>
|
||||
Remove space member
|
||||
{t("Remove space member")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@ -5,10 +5,14 @@ import { useAtom } from "jotai";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import { FileButton, Tooltip } from "@mantine/core";
|
||||
import { uploadAvatar } from "@/features/user/services/user-service.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const userAtom = focusAtom(currentUserAtom, (optic) => optic.prop("user"));
|
||||
|
||||
export default function AccountAvatar() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "account",
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
const [, setUser] = useAtom(userAtom);
|
||||
@ -36,7 +40,7 @@ export default function AccountAvatar() {
|
||||
<>
|
||||
<FileButton onChange={handleFileChange} accept="image/png,image/jpeg">
|
||||
{(props) => (
|
||||
<Tooltip label="Change photo" position="bottom">
|
||||
<Tooltip label={t("Change photo")} position="bottom">
|
||||
<CustomAvatar
|
||||
{...props}
|
||||
component="button"
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
import { Group, Text, Select } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function AccountLanguage() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "preference",
|
||||
});
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">{t("Language")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("Choose your preferred interface language.")}
|
||||
</Text>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
function LanguageSwitcher() {
|
||||
const { t, i18n } = useTranslation("settings", {
|
||||
keyPrefix: "preference",
|
||||
});
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
i18n.changeLanguage(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
label={t("Select language")}
|
||||
data={[
|
||||
{ value: "zh", label: "中文" },
|
||||
{ value: "en", label: "English" },
|
||||
]}
|
||||
value={i18n.language}
|
||||
onChange={handleChange}
|
||||
allowDeselect={false}
|
||||
checkIconPosition="right"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -8,6 +8,7 @@ import { IUser } from "@/features/user/types/user.types.ts";
|
||||
import { useState } from "react";
|
||||
import { TextInput, Button } from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2).max(40).nonempty("Your name cannot be blank"),
|
||||
@ -18,6 +19,9 @@ type FormValues = z.infer<typeof formSchema>;
|
||||
const userAtom = focusAtom(currentUserAtom, (optic) => optic.prop("user"));
|
||||
|
||||
export default function AccountNameForm() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "account",
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
const [, setUser] = useAtom(userAtom);
|
||||
@ -36,12 +40,12 @@ export default function AccountNameForm() {
|
||||
const updatedUser = await updateUser(data);
|
||||
setUser(updatedUser);
|
||||
notifications.show({
|
||||
message: "Updated successfully",
|
||||
message: t("Updated successfully"),
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
notifications.show({
|
||||
message: "Failed to update data",
|
||||
message: t("Failed to update data"),
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
@ -53,13 +57,13 @@ export default function AccountNameForm() {
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<TextInput
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="Your name"
|
||||
label={t("Name")}
|
||||
placeholder={t("Your name")}
|
||||
variant="filled"
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<Button type="submit" mt="sm" disabled={isLoading} loading={isLoading}>
|
||||
Save
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
||||
@ -5,14 +5,19 @@ import {
|
||||
Select,
|
||||
MantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function AccountTheme() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "preference",
|
||||
});
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">Theme</Text>
|
||||
<Text size="md">{t("Theme")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Choose your preferred color scheme.
|
||||
{t("Choose your preferred color scheme.")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
@ -22,6 +27,10 @@ export default function AccountTheme() {
|
||||
}
|
||||
|
||||
function ThemeSwitcher() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "preference",
|
||||
});
|
||||
|
||||
const { colorScheme, setColorScheme } = useMantineColorScheme();
|
||||
|
||||
const handleChange = (value: MantineColorScheme) => {
|
||||
@ -30,11 +39,11 @@ function ThemeSwitcher() {
|
||||
|
||||
return (
|
||||
<Select
|
||||
label="Select theme"
|
||||
label={t("Select theme")}
|
||||
data={[
|
||||
{ value: "light", label: "Light" },
|
||||
{ value: "dark", label: "Dark" },
|
||||
{ value: "auto", label: "System settings" },
|
||||
{ value: "light", label: t("Light") },
|
||||
{ value: "dark", label: t("Dark") },
|
||||
{ value: "auto", label: t("System settings") },
|
||||
]}
|
||||
value={colorScheme}
|
||||
onChange={handleChange}
|
||||
|
||||
@ -13,15 +13,19 @@ import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import * as React from "react";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ChangeEmail() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "account",
|
||||
});
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">Email</Text>
|
||||
<Text size="md">{t("Email")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{currentUser?.user.email}
|
||||
</Text>
|
||||
@ -29,13 +33,15 @@ export default function ChangeEmail() {
|
||||
|
||||
{/*
|
||||
<Button onClick={open} variant="default">
|
||||
Change email
|
||||
{t("Change email")}
|
||||
</Button>
|
||||
*/}
|
||||
|
||||
<Modal opened={opened} onClose={close} title="Change email" centered>
|
||||
<Modal opened={opened} onClose={close} title={t("Change email")} centered>
|
||||
<Text mb="md">
|
||||
To change your email, you have to enter your password and new email.
|
||||
{t(
|
||||
"To change your email, you have to enter your password and new email.",
|
||||
)}
|
||||
</Text>
|
||||
<ChangeEmailForm />
|
||||
</Modal>
|
||||
@ -53,6 +59,9 @@ const formSchema = z.object({
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
function ChangeEmailForm() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "account",
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
@ -71,8 +80,8 @@ function ChangeEmailForm() {
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Enter your password"
|
||||
label={t("Password")}
|
||||
placeholder={t("Enter your password")}
|
||||
variant="filled"
|
||||
mb="md"
|
||||
{...form.getInputProps("password")}
|
||||
@ -80,16 +89,16 @@ function ChangeEmailForm() {
|
||||
|
||||
<TextInput
|
||||
id="email"
|
||||
label="Email"
|
||||
description="Enter your new preferred email"
|
||||
placeholder="New email"
|
||||
label={t("Email")}
|
||||
description={t("Enter your new preferred email")}
|
||||
placeholder={t("New email")}
|
||||
variant="filled"
|
||||
mb="md"
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={isLoading} loading={isLoading}>
|
||||
Change email
|
||||
{t("Change email")}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
||||
@ -6,25 +6,36 @@ import * as React from "react";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { changePassword } from "@/features/auth/services/auth-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ChangePassword() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "account",
|
||||
});
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">Password</Text>
|
||||
<Text size="md">{t("Password")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
You can change your password here.
|
||||
{t("You can change your password here.")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Button onClick={open} variant="default">
|
||||
Change password
|
||||
{t("Change password")}
|
||||
</Button>
|
||||
|
||||
<Modal opened={opened} onClose={close} title="Change password" centered>
|
||||
<Text mb="md">Your password must be a minimum of 8 characters.</Text>
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={t("Change password")}
|
||||
centered
|
||||
>
|
||||
<Text mb="md">
|
||||
{t("Your password must be a minimum of 8 characters.")}
|
||||
</Text>
|
||||
<ChangePasswordForm onClose={close} />
|
||||
</Modal>
|
||||
</Group>
|
||||
@ -44,6 +55,9 @@ interface ChangePasswordFormProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
function ChangePasswordForm({ onClose }: ChangePasswordFormProps) {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "account",
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
@ -62,7 +76,7 @@ function ChangePasswordForm({ onClose }: ChangePasswordFormProps) {
|
||||
newPassword: data.newPassword,
|
||||
});
|
||||
notifications.show({
|
||||
message: "Password changed successfully",
|
||||
message: t("Password changed successfully"),
|
||||
});
|
||||
|
||||
onClose();
|
||||
@ -78,9 +92,9 @@ function ChangePasswordForm({ onClose }: ChangePasswordFormProps) {
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<PasswordInput
|
||||
label="Current password"
|
||||
label={t("Current password")}
|
||||
name="oldPassword"
|
||||
placeholder="Enter your current password"
|
||||
placeholder={t("Enter your current password")}
|
||||
variant="filled"
|
||||
mb="md"
|
||||
data-autofocus
|
||||
@ -88,8 +102,8 @@ function ChangePasswordForm({ onClose }: ChangePasswordFormProps) {
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="New password"
|
||||
placeholder="Enter your new password"
|
||||
label={t("New password")}
|
||||
placeholder={t("Enter your new password")}
|
||||
variant="filled"
|
||||
mb="md"
|
||||
{...form.getInputProps("newPassword")}
|
||||
@ -97,7 +111,7 @@ function ChangePasswordForm({ onClose }: ChangePasswordFormProps) {
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button type="submit" disabled={isLoading} loading={isLoading}>
|
||||
Change password
|
||||
{t("Change password")}
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
|
||||
@ -3,14 +3,19 @@ import { useAtom } from "jotai/index";
|
||||
import { userAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { updateUser } from "@/features/user/services/user-service.ts";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function PageWidthPref() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "preference",
|
||||
});
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">Full page width</Text>
|
||||
<Text size="md">{t("Full page width")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Choose your preferred page width.
|
||||
{t("Choose your preferred page width.")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
@ -24,6 +29,9 @@ interface PageWidthToggleProps {
|
||||
label?: string;
|
||||
}
|
||||
export function PageWidthToggle({ size, label }: PageWidthToggleProps) {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "preference",
|
||||
});
|
||||
const [user, setUser] = useAtom(userAtom);
|
||||
const [checked, setChecked] = useState(
|
||||
user.settings?.preferences?.fullPageWidth,
|
||||
@ -43,7 +51,7 @@ export function PageWidthToggle({ size, label }: PageWidthToggleProps) {
|
||||
labelPosition="left"
|
||||
defaultChecked={checked}
|
||||
onChange={handleChange}
|
||||
aria-label="Toggle full page width"
|
||||
aria-label={t("Toggle full page width")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,11 +5,15 @@ import { UserRole } from "@/lib/types.ts";
|
||||
import { userRoleData } from "@/features/workspace/types/user-role-data.ts";
|
||||
import { useCreateInvitationMutation } from "@/features/workspace/queries/workspace-query.ts";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
export function WorkspaceInviteForm({ onClose }: Props) {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.member",
|
||||
});
|
||||
const [emails, setEmails] = useState<string[]>([]);
|
||||
const [role, setRole] = useState<string | null>(UserRole.MEMBER);
|
||||
const [groupIds, setGroupIds] = useState<string[]>([]);
|
||||
@ -44,9 +48,11 @@ export function WorkspaceInviteForm({ onClose }: Props) {
|
||||
|
||||
<TagsInput
|
||||
mt="sm"
|
||||
description="Enter valid email addresses separated by comma or space [max: 50]"
|
||||
label="Invite by email"
|
||||
placeholder="enter valid emails addresses"
|
||||
description={t(
|
||||
"Enter valid email addresses separated by comma or space max_50",
|
||||
)}
|
||||
label={t("Invite by email")}
|
||||
placeholder={t("enter valid emails addresses")}
|
||||
variant="filled"
|
||||
splitChars={[",", " "]}
|
||||
maxDropdownHeight={200}
|
||||
@ -56,9 +62,9 @@ export function WorkspaceInviteForm({ onClose }: Props) {
|
||||
|
||||
<Select
|
||||
mt="sm"
|
||||
description="Select role to assign to all invited members"
|
||||
label="Select role"
|
||||
placeholder="Choose a role"
|
||||
description={t("Select role to assign to all invited members")}
|
||||
label={t("Select role")}
|
||||
placeholder={t("Choose a role")}
|
||||
variant="filled"
|
||||
data={userRoleData.filter((role) => role.value !== UserRole.OWNER)}
|
||||
defaultValue={UserRole.MEMBER}
|
||||
@ -69,8 +75,10 @@ export function WorkspaceInviteForm({ onClose }: Props) {
|
||||
|
||||
<MultiGroupSelect
|
||||
mt="sm"
|
||||
description="Invited members will be granted access to spaces the groups can access"
|
||||
label={"Add to groups"}
|
||||
description={t(
|
||||
"Invited members will be granted access to spaces the groups can access",
|
||||
)}
|
||||
label={t("Add to groups")}
|
||||
onChange={handleGroupSelect}
|
||||
/>
|
||||
|
||||
@ -79,7 +87,7 @@ export function WorkspaceInviteForm({ onClose }: Props) {
|
||||
onClick={handleSubmit}
|
||||
loading={createInvitationMutation.isPending}
|
||||
>
|
||||
Send invitation
|
||||
{t("Send invitation")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
@ -1,19 +1,23 @@
|
||||
import { WorkspaceInviteForm } from "@/features/workspace/components/members/components/workspace-invite-form.tsx";
|
||||
import { Button, Divider, Modal, ScrollArea } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function WorkspaceInviteModal() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.member",
|
||||
});
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={open}>Invite members</Button>
|
||||
<Button onClick={open}>{t("Invite members")}</Button>
|
||||
|
||||
<Modal
|
||||
size="550"
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title="Invite new members"
|
||||
title={t("Invite new members")}
|
||||
centered
|
||||
>
|
||||
<Divider size="xs" mb="xs" />
|
||||
|
||||
@ -6,8 +6,12 @@ import InviteActionMenu from "@/features/workspace/components/members/components
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
import { formattedDate } from "@/lib/time.ts";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function WorkspaceInvitesTable() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.member",
|
||||
});
|
||||
const { data, isLoading } = useWorkspaceInvitationsQuery({
|
||||
limit: 100,
|
||||
});
|
||||
@ -16,7 +20,9 @@ export default function WorkspaceInvitesTable() {
|
||||
return (
|
||||
<>
|
||||
<Alert variant="light" color="blue" icon={<IconInfoCircle />}>
|
||||
Invited members who are yet to accept their invitation will appear here.
|
||||
{t(
|
||||
"Invited members who are yet to accept their invitation will appear here.",
|
||||
)}
|
||||
</Alert>
|
||||
|
||||
{data && (
|
||||
@ -24,9 +30,9 @@ export default function WorkspaceInvitesTable() {
|
||||
<Table verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Email</Table.Th>
|
||||
<Table.Th>Role</Table.Th>
|
||||
<Table.Th>Date</Table.Th>
|
||||
<Table.Th>{t("Email")}</Table.Th>
|
||||
<Table.Th>{t("Role")}</Table.Th>
|
||||
<Table.Th>{t("Date")}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
|
||||
@ -12,13 +12,19 @@ import {
|
||||
} from "@/features/workspace/types/user-role-data.ts";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { UserRole } from "@/lib/types.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function WorkspaceMembersTable() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.member",
|
||||
});
|
||||
const { data, isLoading } = useWorkspaceMembersQuery({ limit: 100 });
|
||||
const changeMemberRoleMutation = useChangeMemberRoleMutation();
|
||||
const { isAdmin, isOwner } = useUserRole();
|
||||
|
||||
const assignableUserRoles = isOwner ? userRoleData : userRoleData.filter((role) => role.value !== UserRole.OWNER);
|
||||
const assignableUserRoles = isOwner
|
||||
? userRoleData
|
||||
: userRoleData.filter((role) => role.value !== UserRole.OWNER);
|
||||
|
||||
const handleRoleChange = async (
|
||||
userId: string,
|
||||
@ -43,9 +49,9 @@ export default function WorkspaceMembersTable() {
|
||||
<Table verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>User</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Role</Table.Th>
|
||||
<Table.Th>{t("User")}</Table.Th>
|
||||
<Table.Th>{t("Status")}</Table.Th>
|
||||
<Table.Th>{t("Role")}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
@ -67,7 +73,7 @@ export default function WorkspaceMembersTable() {
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<Badge variant="light">Active</Badge>
|
||||
<Badge variant="light">{t("Active")}</Badge>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
|
||||
@ -9,6 +9,7 @@ import { TextInput, Button } from "@mantine/core";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(4).nonempty("Workspace name cannot be blank"),
|
||||
@ -21,6 +22,9 @@ const workspaceAtom = focusAtom(currentUserAtom, (optic) =>
|
||||
);
|
||||
|
||||
export default function WorkspaceNameForm() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.general",
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
const [, setWorkspace] = useAtom(workspaceAtom);
|
||||
@ -39,11 +43,11 @@ export default function WorkspaceNameForm() {
|
||||
try {
|
||||
const updatedWorkspace = await updateWorkspace(data);
|
||||
setWorkspace(updatedWorkspace);
|
||||
notifications.show({ message: "Updated successfully" });
|
||||
notifications.show({ message: t("Updated successfully") });
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
notifications.show({
|
||||
message: "Failed to update data",
|
||||
message: t("Failed to update data"),
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
@ -55,8 +59,8 @@ export default function WorkspaceNameForm() {
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<TextInput
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="e.g ACME"
|
||||
label={t("Name")}
|
||||
placeholder={t("e.g ACME")}
|
||||
variant="filled"
|
||||
readOnly={!isAdmin}
|
||||
{...form.getInputProps("name")}
|
||||
@ -69,7 +73,7 @@ export default function WorkspaceNameForm() {
|
||||
disabled={isLoading || !form.isDirty()}
|
||||
loading={isLoading}
|
||||
>
|
||||
Save
|
||||
{t("Save")}
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
|
||||
34
apps/client/src/i18n.ts
Normal file
34
apps/client/src/i18n.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
import Backend from "i18next-http-backend";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
// don't want to use this?
|
||||
// have a look at the Quick start guide
|
||||
// for passing in lng and translations on init
|
||||
|
||||
i18n
|
||||
// load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales)
|
||||
// learn more: https://github.com/i18next/i18next-http-backend
|
||||
// want your translations to be loaded from a professional CDN? => https://github.com/locize/react-tutorial#step-2---use-the-locize-cdn
|
||||
.use(Backend)
|
||||
// detect user language
|
||||
// learn more: https://github.com/i18next/i18next-browser-languageDetector
|
||||
.use(LanguageDetector)
|
||||
// pass the i18n instance to react-i18next.
|
||||
.use(initReactI18next)
|
||||
// init i18next
|
||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||
.init({
|
||||
fallbackLng: "en",
|
||||
debug: true,
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
react: {
|
||||
useSuspense: false,
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@ -1,7 +1,6 @@
|
||||
import "@mantine/core/styles.css";
|
||||
import "@mantine/spotlight/styles.css";
|
||||
import "@mantine/notifications/styles.css";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import { theme } from "@/theme";
|
||||
@ -11,6 +10,7 @@ import { ModalsProvider } from "@mantine/modals";
|
||||
import { Notifications } from "@mantine/notifications";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { HelmetProvider } from "react-helmet-async";
|
||||
import "./i18n";
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@ -24,7 +24,7 @@ export const queryClient = new QueryClient({
|
||||
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById("root") as HTMLElement,
|
||||
document.getElementById("root") as HTMLElement
|
||||
);
|
||||
|
||||
root.render(
|
||||
@ -39,5 +39,5 @@ root.render(
|
||||
</QueryClientProvider>
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</BrowserRouter>,
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { InviteSignUpForm } from "@/features/auth/components/invite-sign-up-form.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function InviteSignup() {
|
||||
const { t } = useTranslation("invite-signup");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Invitation signup</title>
|
||||
<title>{t("Invitation signup")}</title>
|
||||
</Helmet>
|
||||
<InviteSignUpForm />
|
||||
</>
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import { LoginForm } from "@/features/auth/components/login-form";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function LoginPage() {
|
||||
const { t } = useTranslation("login");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Login</title>
|
||||
<title>{t("Login")}</title>
|
||||
</Helmet>
|
||||
<LoginForm />
|
||||
</>
|
||||
|
||||
@ -3,8 +3,10 @@ import { SetupWorkspaceForm } from "@/features/auth/components/setup-workspace-f
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import React, { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function SetupWorkspace() {
|
||||
const { t } = useTranslation("setup-workspace");
|
||||
const {
|
||||
data: workspace,
|
||||
isLoading,
|
||||
@ -32,7 +34,7 @@ export default function SetupWorkspace() {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Setup workspace</title>
|
||||
<title>{t("Setup workspace")}</title>
|
||||
</Helmet>
|
||||
<SetupWorkspaceForm />
|
||||
</>
|
||||
|
||||
@ -12,8 +12,10 @@ import {
|
||||
SpaceCaslAction,
|
||||
SpaceCaslSubject,
|
||||
} from "@/features/space/permissions/permissions.type.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function Page() {
|
||||
const { t } = useTranslation("translation", { keyPrefix: "page" });
|
||||
const { pageSlug } = useParams();
|
||||
const {
|
||||
data: page,
|
||||
@ -31,7 +33,7 @@ export default function Page() {
|
||||
|
||||
if (isError || !page) {
|
||||
// TODO: fix this
|
||||
return <div>Error fetching page data.</div>;
|
||||
return <div>{t("Error fetching page data.")}</div>;
|
||||
}
|
||||
|
||||
if (!space) {
|
||||
@ -42,7 +44,7 @@ export default function Page() {
|
||||
page && (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>{`${page?.icon || ""} ${page?.title || "untitled"}`}</title>
|
||||
<title>{`${page?.icon || ""} ${page?.title || t("untitled")}`}</title>
|
||||
</Helmet>
|
||||
|
||||
<PageHeader
|
||||
|
||||
@ -1,14 +1,22 @@
|
||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||
import AccountLanguage from "@/features/user/components/account-languate";
|
||||
import AccountTheme from "@/features/user/components/account-theme.tsx";
|
||||
import PageWidthPref from "@/features/user/components/page-width-pref.tsx";
|
||||
import { Divider } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function AccountPreferences() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "preference",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsTitle title="Preferences" />
|
||||
<SettingsTitle title={t("Preferences")} />
|
||||
<AccountTheme />
|
||||
<Divider my={"md"} />
|
||||
<AccountLanguage />
|
||||
<Divider my={"md"} />
|
||||
<PageWidthPref />
|
||||
</>
|
||||
);
|
||||
|
||||
@ -4,11 +4,16 @@ import ChangePassword from "@/features/user/components/change-password";
|
||||
import { Divider } from "@mantine/core";
|
||||
import AccountAvatar from "@/features/user/components/account-avatar";
|
||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function AccountSettings() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "account",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsTitle title="My Profile" />
|
||||
<SettingsTitle title={t("My Profile")} />
|
||||
|
||||
<AccountAvatar />
|
||||
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||
import GroupMembersList from "@/features/group/components/group-members";
|
||||
import GroupDetails from "@/features/group/components/group-details";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function GroupInfo() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.group",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsTitle title="Manage Group" />
|
||||
<SettingsTitle title={t("Manage Group")} />
|
||||
<GroupDetails />
|
||||
<GroupMembersList />
|
||||
</>
|
||||
|
||||
@ -3,13 +3,17 @@ import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||
import { Group } from "@mantine/core";
|
||||
import CreateGroupModal from "@/features/group/components/create-group-modal";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function Groups() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.group",
|
||||
});
|
||||
const { isAdmin } = useUserRole();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsTitle title="Groups" />
|
||||
<SettingsTitle title={t("Groups")} />
|
||||
|
||||
<Group my="md" justify="flex-end">
|
||||
{isAdmin && <CreateGroupModal />}
|
||||
|
||||
@ -3,13 +3,17 @@ import SpaceList from "@/features/space/components/space-list.tsx";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { Group } from "@mantine/core";
|
||||
import CreateSpaceModal from "@/features/space/components/create-space-modal.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function Spaces() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.space",
|
||||
});
|
||||
const { isAdmin } = useUserRole();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsTitle title="Spaces" />
|
||||
<SettingsTitle title={t("Spaces")} />
|
||||
|
||||
<Group my="md" justify="flex-end">
|
||||
{isAdmin && <CreateSpaceModal />}
|
||||
|
||||
@ -6,8 +6,12 @@ import { useEffect, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import WorkspaceInvitesTable from "@/features/workspace/components/members/components/workspace-invites-table.tsx";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function WorkspaceMembers() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.member",
|
||||
});
|
||||
const [segmentValue, setSegmentValue] = useState("members");
|
||||
const [searchParams] = useSearchParams();
|
||||
const { isAdmin } = useUserRole();
|
||||
@ -31,7 +35,7 @@ export default function WorkspaceMembers() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsTitle title="Members" />
|
||||
<SettingsTitle title={t("Members")} />
|
||||
|
||||
{/* <WorkspaceInviteSection /> */}
|
||||
{/* <Divider my="lg" /> */}
|
||||
@ -41,8 +45,8 @@ export default function WorkspaceMembers() {
|
||||
value={segmentValue}
|
||||
onChange={handleSegmentChange}
|
||||
data={[
|
||||
{ label: "Members", value: "members" },
|
||||
{ label: "Pending", value: "invites" },
|
||||
{ label: t("Members"), value: "members" },
|
||||
{ label: t("Pending"), value: "invites" },
|
||||
]}
|
||||
withItemsBorders={false}
|
||||
/>
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||
import WorkspaceNameForm from "@/features/workspace/components/settings/components/workspace-name-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function WorkspaceSettings() {
|
||||
const { t } = useTranslation("settings", {
|
||||
keyPrefix: "workspace.general",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsTitle title="General" />
|
||||
<SettingsTitle title={t("General")} />
|
||||
<WorkspaceNameForm />
|
||||
</>
|
||||
);
|
||||
|
||||
@ -78,5 +78,8 @@
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"semi": true
|
||||
}
|
||||
}
|
||||
|
||||
18159
pnpm-lock.yaml
generated
18159
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user