mirror of
https://github.com/docmost/docmost.git
synced 2025-11-26 00:01:11 +10:00
feat: support i18n
This commit is contained in:
@ -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 &&
|
||||
<div>
|
||||
<HistoryEditor content={data.content} title={data.title} />
|
||||
</div>
|
||||
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 />
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user