feat: localization (#196)
* Started adding locale translations :) * Added some more translations * Working on translating even more pages * More translations * Added test default locale retrieval * replace `intl.formatMessage` with custom `t` hook * add more translations * improve title syntax * add more translations * translate admin config page * translated error messages * add language selecter * minor fixes * improve language handling * add upcoming languages * add `crowdin.yml` * run formatter --------- Co-authored-by: Steve Tautonico <stautonico@gmail.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import Meta from "../components/Meta";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
root: {
|
||||
@@ -42,9 +43,11 @@ const ErrorNotFound = () => {
|
||||
<>
|
||||
<Meta title="Not found" />
|
||||
<Container className={classes.root}>
|
||||
<div className={classes.label}>404</div>
|
||||
<div className={classes.label}>
|
||||
<FormattedMessage id="404.title" />
|
||||
</div>
|
||||
<Title align="center" order={3}>
|
||||
Oops this page doesn't exist.
|
||||
<FormattedMessage id="404.description" />
|
||||
</Title>
|
||||
<Text
|
||||
color="dimmed"
|
||||
@@ -53,7 +56,7 @@ const ErrorNotFound = () => {
|
||||
></Text>
|
||||
<Group position="center">
|
||||
<Button component={Link} href="/" variant="light">
|
||||
Bring me back
|
||||
<FormattedMessage id="404.button.home" />
|
||||
</Button>
|
||||
</Group>
|
||||
</Container>
|
||||
|
||||
@@ -13,11 +13,12 @@ import { GetServerSidePropsContext } from "next";
|
||||
import type { AppProps } from "next/app";
|
||||
import getConfig from "next/config";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { IntlProvider } from "react-intl";
|
||||
import Header from "../components/header/Header";
|
||||
import { ConfigContext } from "../hooks/config.hook";
|
||||
import usePreferences from "../hooks/usePreferences";
|
||||
import { UserContext } from "../hooks/user.hook";
|
||||
import { LOCALES } from "../i18n/locales";
|
||||
import authService from "../services/auth.service";
|
||||
import configService from "../services/config.service";
|
||||
import userService from "../services/user.service";
|
||||
@@ -25,6 +26,8 @@ import GlobalStyle from "../styles/global.style";
|
||||
import globalStyle from "../styles/mantine.style";
|
||||
import Config from "../types/config.type";
|
||||
import { CurrentUser } from "../types/user.type";
|
||||
import i18nUtil from "../utils/i18n.util";
|
||||
import userPreferences from "../utils/userPreferences.util";
|
||||
|
||||
const excludeDefaultLayoutRoutes = ["/admin/config/[category]"];
|
||||
|
||||
@@ -33,7 +36,6 @@ function App({ Component, pageProps }: AppProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const [colorScheme, setColorScheme] = useState<ColorScheme>(systemTheme);
|
||||
const preferences = usePreferences();
|
||||
|
||||
const [user, setUser] = useState<CurrentUser | null>(pageProps.user);
|
||||
const [route, setRoute] = useState<string>(pageProps.route);
|
||||
@@ -50,11 +52,20 @@ function App({ Component, pageProps }: AppProps) {
|
||||
setInterval(async () => await authService.refreshAccessToken(), 30 * 1000);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageProps.language) return;
|
||||
const cookieLanguage = getCookie("language");
|
||||
if (pageProps.language != cookieLanguage) {
|
||||
i18nUtil.setLanguageCookie(pageProps.language);
|
||||
if (cookieLanguage) location.reload();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const colorScheme =
|
||||
preferences.get("colorScheme") == "system"
|
||||
userPreferences.get("colorScheme") == "system"
|
||||
? systemTheme
|
||||
: preferences.get("colorScheme");
|
||||
: userPreferences.get("colorScheme");
|
||||
|
||||
toggleColorScheme(colorScheme);
|
||||
}, [systemTheme]);
|
||||
@@ -66,52 +77,60 @@ function App({ Component, pageProps }: AppProps) {
|
||||
});
|
||||
};
|
||||
|
||||
const language = useRef(pageProps.language);
|
||||
|
||||
return (
|
||||
<MantineProvider
|
||||
withGlobalStyles
|
||||
withNormalizeCSS
|
||||
theme={{ colorScheme, ...globalStyle }}
|
||||
<IntlProvider
|
||||
messages={i18nUtil.getLocaleByCode(language.current)?.messages}
|
||||
locale={language.current}
|
||||
defaultLocale={LOCALES.ENGLISH.code}
|
||||
>
|
||||
<ColorSchemeProvider
|
||||
colorScheme={colorScheme}
|
||||
toggleColorScheme={toggleColorScheme}
|
||||
<MantineProvider
|
||||
withGlobalStyles
|
||||
withNormalizeCSS
|
||||
theme={{ colorScheme, ...globalStyle }}
|
||||
>
|
||||
<GlobalStyle />
|
||||
<Notifications />
|
||||
<ModalsProvider>
|
||||
<ConfigContext.Provider
|
||||
value={{
|
||||
configVariables,
|
||||
refresh: async () => {
|
||||
setConfigVariables(await configService.list());
|
||||
},
|
||||
}}
|
||||
>
|
||||
<UserContext.Provider
|
||||
<ColorSchemeProvider
|
||||
colorScheme={colorScheme}
|
||||
toggleColorScheme={toggleColorScheme}
|
||||
>
|
||||
<GlobalStyle />
|
||||
<Notifications />
|
||||
<ModalsProvider>
|
||||
<ConfigContext.Provider
|
||||
value={{
|
||||
user,
|
||||
refreshUser: async () => {
|
||||
const user = await userService.getCurrentUser();
|
||||
setUser(user);
|
||||
return user;
|
||||
configVariables,
|
||||
refresh: async () => {
|
||||
setConfigVariables(await configService.list());
|
||||
},
|
||||
}}
|
||||
>
|
||||
{excludeDefaultLayoutRoutes.includes(route) ? (
|
||||
<Component {...pageProps} />
|
||||
) : (
|
||||
<>
|
||||
<Header />
|
||||
<Container>
|
||||
<Component {...pageProps} />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
</UserContext.Provider>
|
||||
</ConfigContext.Provider>
|
||||
</ModalsProvider>
|
||||
</ColorSchemeProvider>
|
||||
</MantineProvider>
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
user,
|
||||
refreshUser: async () => {
|
||||
const user = await userService.getCurrentUser();
|
||||
setUser(user);
|
||||
return user;
|
||||
},
|
||||
}}
|
||||
>
|
||||
{excludeDefaultLayoutRoutes.includes(route) ? (
|
||||
<Component {...pageProps} />
|
||||
) : (
|
||||
<>
|
||||
<Header />
|
||||
<Container>
|
||||
<Component {...pageProps} />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
</UserContext.Provider>
|
||||
</ConfigContext.Provider>
|
||||
</ModalsProvider>
|
||||
</ColorSchemeProvider>
|
||||
</MantineProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,11 +144,13 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
||||
configVariables?: Config[];
|
||||
route?: string;
|
||||
colorScheme: ColorScheme;
|
||||
language?: string;
|
||||
} = {
|
||||
route: ctx.resolvedUrl,
|
||||
colorScheme:
|
||||
(getCookie("mantine-color-scheme", ctx) as ColorScheme) ?? "light",
|
||||
};
|
||||
|
||||
if (ctx.req) {
|
||||
const cookieHeader = ctx.req.headers.cookie;
|
||||
|
||||
@@ -142,8 +163,13 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
||||
pageProps.configVariables = (await axios(`${apiURL}/api/configs`)).data;
|
||||
|
||||
pageProps.route = ctx.req.url;
|
||||
}
|
||||
|
||||
const requestLanguage = i18nUtil.getLanguageFromAcceptHeader(
|
||||
ctx.req.headers["accept-language"]
|
||||
);
|
||||
|
||||
pageProps.language = ctx.req.cookies["language"] ?? requestLanguage;
|
||||
}
|
||||
return { pageProps };
|
||||
};
|
||||
|
||||
|
||||
@@ -14,18 +14,22 @@ import {
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { Tb2Fa } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
import showEnableTotpModal from "../../components/account/showEnableTotpModal";
|
||||
import ThemeSwitcher from "../../components/account/ThemeSwitcher";
|
||||
import Meta from "../../components/Meta";
|
||||
import ThemeSwitcher from "../../components/account/ThemeSwitcher";
|
||||
import showEnableTotpModal from "../../components/account/showEnableTotpModal";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
import authService from "../../services/auth.service";
|
||||
import userService from "../../services/user.service";
|
||||
import toast from "../../utils/toast.util";
|
||||
import LanguagePicker from "../../components/account/LanguagePicker";
|
||||
|
||||
const Account = () => {
|
||||
const { user, refreshUser } = useUser();
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const accountForm = useForm({
|
||||
initialValues: {
|
||||
@@ -34,8 +38,10 @@ const Account = () => {
|
||||
},
|
||||
validate: yupResolver(
|
||||
yup.object().shape({
|
||||
email: yup.string().email(),
|
||||
username: yup.string().min(3),
|
||||
email: yup.string().email(t("common.error.invalid-email")),
|
||||
username: yup
|
||||
.string()
|
||||
.min(3, t("common.error.too-short", { length: 3 })),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -47,8 +53,14 @@ const Account = () => {
|
||||
},
|
||||
validate: yupResolver(
|
||||
yup.object().shape({
|
||||
oldPassword: yup.string().min(8),
|
||||
password: yup.string().min(8),
|
||||
oldPassword: yup
|
||||
.string()
|
||||
.min(8, t("common.error.too-short", { length: 8 }))
|
||||
.required(t("common.error.field-required")),
|
||||
password: yup
|
||||
.string()
|
||||
.min(8, t("common.error.too-short", { length: 8 }))
|
||||
.required(t("common.error.field-required")),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -59,7 +71,10 @@ const Account = () => {
|
||||
},
|
||||
validate: yupResolver(
|
||||
yup.object().shape({
|
||||
password: yup.string().min(8),
|
||||
password: yup
|
||||
.string()
|
||||
.min(8, t("common.error.too-short", { length: 8 }))
|
||||
.required(t("common.error.field-required")),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -74,23 +89,23 @@ const Account = () => {
|
||||
password: yup.string().min(8),
|
||||
code: yup
|
||||
.string()
|
||||
.min(6)
|
||||
.max(6)
|
||||
.matches(/^[0-9]+$/, { message: "Code must be a number" }),
|
||||
.min(6, t("common.error.exact-length", { length: 6 }))
|
||||
.max(6, t("common.error.exact-length", { length: 6 }))
|
||||
.matches(/^[0-9]+$/, { message: t("common.error.invalid-number") }),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="My account" />
|
||||
<Meta title={t("account.title")} />
|
||||
<Container size="sm">
|
||||
<Title order={3} mb="xs">
|
||||
My account
|
||||
<FormattedMessage id="account.title" />
|
||||
</Title>
|
||||
<Paper withBorder p="xl">
|
||||
<Title order={5} mb="xs">
|
||||
Account Info
|
||||
<FormattedMessage id="account.card.info.title" />
|
||||
</Title>
|
||||
<form
|
||||
onSubmit={accountForm.onSubmit((values) =>
|
||||
@@ -99,35 +114,37 @@ const Account = () => {
|
||||
username: values.username,
|
||||
email: values.email,
|
||||
})
|
||||
.then(() => toast.success("User updated successfully"))
|
||||
.then(() => toast.success(t("account.notify.info.success")))
|
||||
.catch(toast.axiosError)
|
||||
)}
|
||||
>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label="Username"
|
||||
label={t("account.card.info.username")}
|
||||
{...accountForm.getInputProps("username")}
|
||||
/>
|
||||
<TextInput
|
||||
label="Email"
|
||||
label={t("account.card.info.email")}
|
||||
{...accountForm.getInputProps("email")}
|
||||
/>
|
||||
<Group position="right">
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="submit">
|
||||
<FormattedMessage id="common.button.save" />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Paper>
|
||||
<Paper withBorder p="xl" mt="lg">
|
||||
<Title order={5} mb="xs">
|
||||
Password
|
||||
<FormattedMessage id="account.card.password.title" />
|
||||
</Title>
|
||||
<form
|
||||
onSubmit={passwordForm.onSubmit((values) =>
|
||||
authService
|
||||
.updatePassword(values.oldPassword, values.password)
|
||||
.then(() => {
|
||||
toast.success("Password updated successfully");
|
||||
toast.success(t("account.notify.password.success"));
|
||||
passwordForm.reset();
|
||||
})
|
||||
.catch(toast.axiosError)
|
||||
@@ -135,15 +152,17 @@ const Account = () => {
|
||||
>
|
||||
<Stack>
|
||||
<PasswordInput
|
||||
label="Old password"
|
||||
label={t("account.card.password.old")}
|
||||
{...passwordForm.getInputProps("oldPassword")}
|
||||
/>
|
||||
<PasswordInput
|
||||
label="New password"
|
||||
label={t("account.card.password.new")}
|
||||
{...passwordForm.getInputProps("password")}
|
||||
/>
|
||||
<Group position="right">
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="submit">
|
||||
<FormattedMessage id="common.button.save" />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
@@ -151,7 +170,7 @@ const Account = () => {
|
||||
|
||||
<Paper withBorder p="xl" mt="lg">
|
||||
<Title order={5} mb="xs">
|
||||
Security
|
||||
<FormattedMessage id="account.card.security.title" />
|
||||
</Title>
|
||||
|
||||
<Tabs defaultValue="totp">
|
||||
@@ -169,7 +188,7 @@ const Account = () => {
|
||||
authService
|
||||
.disableTOTP(values.code, values.password)
|
||||
.then(() => {
|
||||
toast.success("Successfully disabled TOTP");
|
||||
toast.success(t("account.notify.totp.disable"));
|
||||
values.password = "";
|
||||
values.code = "";
|
||||
refreshUser();
|
||||
@@ -179,21 +198,23 @@ const Account = () => {
|
||||
>
|
||||
<Stack>
|
||||
<PasswordInput
|
||||
description="Enter your current password to disable TOTP"
|
||||
label="Password"
|
||||
description={t(
|
||||
"account.card.security.totp.disable.description"
|
||||
)}
|
||||
label={t("account.card.password.title")}
|
||||
{...disableTotpForm.getInputProps("password")}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
variant="filled"
|
||||
label="Code"
|
||||
label={t("account.modal.totp.code")}
|
||||
placeholder="******"
|
||||
{...disableTotpForm.getInputProps("code")}
|
||||
/>
|
||||
|
||||
<Group position="right">
|
||||
<Button color="red" type="submit">
|
||||
Disable
|
||||
<FormattedMessage id="common.button.disable" />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
@@ -218,12 +239,16 @@ const Account = () => {
|
||||
>
|
||||
<Stack>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
description="Enter your current password to start enabling TOTP"
|
||||
label={t("account.card.password.title")}
|
||||
description={t(
|
||||
"account.card.security.totp.enable.description"
|
||||
)}
|
||||
{...enableTotpForm.getInputProps("password")}
|
||||
/>
|
||||
<Group position="right">
|
||||
<Button type="submit">Start</Button>
|
||||
<Button type="submit">
|
||||
<FormattedMessage id="account.card.security.totp.button.start" />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
@@ -234,7 +259,13 @@ const Account = () => {
|
||||
</Paper>
|
||||
<Paper withBorder p="xl" mt="lg">
|
||||
<Title order={5} mb="xs">
|
||||
Color scheme
|
||||
<FormattedMessage id="account.card.language.title" />
|
||||
</Title>
|
||||
<LanguagePicker />
|
||||
</Paper>
|
||||
<Paper withBorder p="xl" mt="lg">
|
||||
<Title order={5} mb="xs">
|
||||
<FormattedMessage id="account.card.color.title" />
|
||||
</Title>
|
||||
<ThemeSwitcher />
|
||||
</Paper>
|
||||
@@ -245,15 +276,17 @@ const Account = () => {
|
||||
color="red"
|
||||
onClick={() =>
|
||||
modals.openConfirmModal({
|
||||
title: "Account deletion",
|
||||
title: t("account.modal.delete.title"),
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Do you really want to delete your account including all
|
||||
your active shares?
|
||||
<FormattedMessage id="account.modal.delete.description" />
|
||||
</Text>
|
||||
),
|
||||
|
||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
||||
labels: {
|
||||
confirm: t("common.button.delete"),
|
||||
cancel: t("common.button.cancel"),
|
||||
},
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: async () => {
|
||||
await userService.removeCurrentUser();
|
||||
@@ -262,7 +295,7 @@ const Account = () => {
|
||||
})
|
||||
}
|
||||
>
|
||||
Delete Account
|
||||
<FormattedMessage id="account.button.delete" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Center>
|
||||
|
||||
@@ -17,12 +17,14 @@ import { useModals } from "@mantine/modals";
|
||||
import moment from "moment";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TbInfoCircle, TbLink, TbPlus, TbTrash } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../components/Meta";
|
||||
import showReverseShareLinkModal from "../../components/account/showReverseShareLinkModal";
|
||||
import showShareLinkModal from "../../components/account/showShareLinkModal";
|
||||
import CenterLoader from "../../components/core/CenterLoader";
|
||||
import showCreateReverseShareModal from "../../components/share/modals/showCreateReverseShareModal";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import shareService from "../../services/share.service";
|
||||
import { MyReverseShare } from "../../types/share.type";
|
||||
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||
@@ -31,13 +33,14 @@ import toast from "../../utils/toast.util";
|
||||
const MyShares = () => {
|
||||
const modals = useModals();
|
||||
const clipboard = useClipboard();
|
||||
const t = useTranslate();
|
||||
|
||||
const config = useConfig();
|
||||
|
||||
const [reverseShares, setReverseShares] = useState<MyReverseShare[]>();
|
||||
|
||||
const appUrl = config.get("general.appUrl");
|
||||
|
||||
const [reverseShares, setReverseShares] = useState<MyReverseShare[]>();
|
||||
|
||||
const getReverseShares = () => {
|
||||
shareService
|
||||
.getMyReverseShares()
|
||||
@@ -51,15 +54,17 @@ const MyShares = () => {
|
||||
if (!reverseShares) return <CenterLoader />;
|
||||
return (
|
||||
<>
|
||||
<Meta title="My shares" />
|
||||
<Meta title={t("account.reverseShares.title")} />
|
||||
<Group position="apart" align="baseline" mb={20}>
|
||||
<Group align="center" spacing={3} mb={30}>
|
||||
<Title order={3}>My reverse shares</Title>
|
||||
<Title order={3}>
|
||||
<FormattedMessage id="account.reverseShares.title" />
|
||||
</Title>
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
multiline
|
||||
width={220}
|
||||
label="A reverse share allows you to generate a unique URL that allows external users to create a share."
|
||||
label={t("account.reverseShares.description")}
|
||||
events={{ hover: true, focus: false, touch: true }}
|
||||
>
|
||||
<ActionIcon>
|
||||
@@ -77,14 +82,18 @@ const MyShares = () => {
|
||||
}
|
||||
leftIcon={<TbPlus size={20} />}
|
||||
>
|
||||
Create
|
||||
<FormattedMessage id="common.button.create" />
|
||||
</Button>
|
||||
</Group>
|
||||
{reverseShares.length == 0 ? (
|
||||
<Center style={{ height: "70vh" }}>
|
||||
<Stack align="center" spacing={10}>
|
||||
<Title order={3}>It's empty here 👀</Title>
|
||||
<Text>You don't have any reverse shares.</Text>
|
||||
<Title order={3}>
|
||||
<FormattedMessage id="account.reverseShares.title.empty" />
|
||||
</Title>
|
||||
<Text>
|
||||
<FormattedMessage id="account.reverseShares.description.empty" />
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
) : (
|
||||
@@ -92,10 +101,18 @@ const MyShares = () => {
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Shares</th>
|
||||
<th>Remaining uses</th>
|
||||
<th>Max share size</th>
|
||||
<th>Expires at</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.reverseShares.table.shares" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.reverseShares.table.remaining" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.reverseShares.table.max-size" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.reverseShares.table.expires" />
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -105,7 +122,7 @@ const MyShares = () => {
|
||||
<td style={{ width: 220 }}>
|
||||
{reverseShare.shares.length == 0 ? (
|
||||
<Text color="dimmed" size="sm">
|
||||
No shares created yet
|
||||
<FormattedMessage id="account.reverseShares.table.no-shares" />
|
||||
</Text>
|
||||
) : (
|
||||
<Accordion>
|
||||
@@ -115,9 +132,13 @@ const MyShares = () => {
|
||||
>
|
||||
<Accordion.Control p={0}>
|
||||
<Text size="sm">
|
||||
{`${reverseShare.shares.length} share${
|
||||
reverseShare.shares.length > 1 ? "s" : ""
|
||||
}`}
|
||||
{reverseShare.shares.length == 1
|
||||
? `1 ${t(
|
||||
"account.reverseShares.table.count.singular"
|
||||
)}`
|
||||
: `${reverseShare.shares.length} ${t(
|
||||
"account.reverseShares.table.count.plural"
|
||||
)}`}
|
||||
</Text>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
@@ -140,9 +161,7 @@ const MyShares = () => {
|
||||
clipboard.copy(
|
||||
`${appUrl}/share/${share.id}`
|
||||
);
|
||||
toast.success(
|
||||
"The share link was copied to the keyboard."
|
||||
);
|
||||
toast.success(t("common.notify.copied"));
|
||||
} else {
|
||||
showShareLinkModal(
|
||||
modals,
|
||||
@@ -183,9 +202,7 @@ const MyShares = () => {
|
||||
reverseShare.token
|
||||
}`
|
||||
);
|
||||
toast.success(
|
||||
"The link was copied to your clipboard."
|
||||
);
|
||||
toast.success(t("common.notify.copied"));
|
||||
} else {
|
||||
showReverseShareLinkModal(
|
||||
modals,
|
||||
@@ -203,18 +220,21 @@ const MyShares = () => {
|
||||
size={25}
|
||||
onClick={() => {
|
||||
modals.openConfirmModal({
|
||||
title: `Delete reverse share`,
|
||||
title: t(
|
||||
"account.reverseShares.modal.delete.title"
|
||||
),
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Do you really want to delete this reverse share?
|
||||
If you do, the associated shares will be deleted
|
||||
as well.
|
||||
<FormattedMessage id="account.reverseShares.modal.delete.description" />
|
||||
</Text>
|
||||
),
|
||||
confirmProps: {
|
||||
color: "red",
|
||||
},
|
||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
||||
labels: {
|
||||
confirm: t("common.button.delete"),
|
||||
cancel: t("common.button.cancel"),
|
||||
},
|
||||
onConfirm: () => {
|
||||
shareService.removeReverseShare(reverseShare.id);
|
||||
setReverseShares(
|
||||
|
||||
@@ -17,11 +17,13 @@ import moment from "moment";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TbInfoCircle, TbLink, TbTrash } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../components/Meta";
|
||||
import showShareInformationsModal from "../../components/account/showShareInformationsModal";
|
||||
import showShareLinkModal from "../../components/account/showShareLinkModal";
|
||||
import CenterLoader from "../../components/core/CenterLoader";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import shareService from "../../services/share.service";
|
||||
import { MyShare } from "../../types/share.type";
|
||||
import toast from "../../utils/toast.util";
|
||||
@@ -30,6 +32,7 @@ const MyShares = () => {
|
||||
const modals = useModals();
|
||||
const clipboard = useClipboard();
|
||||
const config = useConfig();
|
||||
const t = useTranslate();
|
||||
|
||||
const [shares, setShares] = useState<MyShare[]>();
|
||||
|
||||
@@ -41,18 +44,22 @@ const MyShares = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="My shares" />
|
||||
<Meta title={t("account.shares.title")} />
|
||||
<Title mb={30} order={3}>
|
||||
My shares
|
||||
<FormattedMessage id="account.shares.title" />
|
||||
</Title>
|
||||
{shares.length == 0 ? (
|
||||
<Center style={{ height: "70vh" }}>
|
||||
<Stack align="center" spacing={10}>
|
||||
<Title order={3}>It's empty here 👀</Title>
|
||||
<Text>You don't have any shares.</Text>
|
||||
<Title order={3}>
|
||||
<FormattedMessage id="account.shares.title.empty" />
|
||||
</Title>
|
||||
<Text>
|
||||
<FormattedMessage id="account.shares.description.empty" />
|
||||
</Text>
|
||||
<Space h={5} />
|
||||
<Button component={Link} href="/upload" variant="light">
|
||||
Create one
|
||||
<FormattedMessage id="account.shares.button.create" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Center>
|
||||
@@ -61,13 +68,21 @@ const MyShares = () => {
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.name" />
|
||||
</th>
|
||||
<MediaQuery smallerThan="md" styles={{ display: "none" }}>
|
||||
<th>Description</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.description" />
|
||||
</th>
|
||||
</MediaQuery>
|
||||
|
||||
<th>Visitors</th>
|
||||
<th>Expires at</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.visitors" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.expiresAt" />
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -121,9 +136,7 @@ const MyShares = () => {
|
||||
share.id
|
||||
}`
|
||||
);
|
||||
toast.success(
|
||||
"The link was copied to your clipboard."
|
||||
);
|
||||
toast.success(t("common.notify.copied"));
|
||||
} else {
|
||||
showShareLinkModal(
|
||||
modals,
|
||||
@@ -141,16 +154,21 @@ const MyShares = () => {
|
||||
size={25}
|
||||
onClick={() => {
|
||||
modals.openConfirmModal({
|
||||
title: `Delete share ${share.id}`,
|
||||
title: t("account.shares.modal.delete.title", {
|
||||
share: share.id,
|
||||
}),
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Do you really want to delete this share?
|
||||
<FormattedMessage id="account.shares.modal.delete.description" />
|
||||
</Text>
|
||||
),
|
||||
confirmProps: {
|
||||
color: "red",
|
||||
},
|
||||
labels: { confirm: "Confirm", cancel: "Cancel" },
|
||||
labels: {
|
||||
confirm: t("common.button.delete"),
|
||||
cancel: t("common.button.cancel"),
|
||||
},
|
||||
onConfirm: () => {
|
||||
shareService.remove(share.id);
|
||||
setShares(
|
||||
|
||||
@@ -13,25 +13,28 @@ import { useMediaQuery } from "@mantine/hooks";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../../components/Meta";
|
||||
import AdminConfigInput from "../../../components/admin/configuration/AdminConfigInput";
|
||||
import ConfigurationHeader from "../../../components/admin/configuration/ConfigurationHeader";
|
||||
import ConfigurationNavBar from "../../../components/admin/configuration/ConfigurationNavBar";
|
||||
import LogoConfigInput from "../../../components/admin/configuration/LogoConfigInput";
|
||||
import TestEmailButton from "../../../components/admin/configuration/TestEmailButton";
|
||||
import CenterLoader from "../../../components/core/CenterLoader";
|
||||
import Meta from "../../../components/Meta";
|
||||
import useConfig from "../../../hooks/config.hook";
|
||||
import configService from "../../../services/config.service";
|
||||
import { AdminConfig, UpdateConfig } from "../../../types/config.type";
|
||||
import {
|
||||
camelToKebab,
|
||||
capitalizeFirstLetter,
|
||||
configVariableToFriendlyName,
|
||||
} from "../../../utils/string.util";
|
||||
import toast from "../../../utils/toast.util";
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
|
||||
export default function AppShellDemo() {
|
||||
const theme = useMantineTheme();
|
||||
const router = useRouter();
|
||||
const t = useTranslate();
|
||||
|
||||
const [isMobileNavBarOpened, setIsMobileNavBarOpened] = useState(false);
|
||||
const isMobile = useMediaQuery("(max-width: 560px)");
|
||||
@@ -94,7 +97,7 @@ export default function AppShellDemo() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="Configuration" />
|
||||
<Meta title={t("admin.config.title")} />
|
||||
<AppShell
|
||||
styles={{
|
||||
main: {
|
||||
@@ -134,26 +137,28 @@ export default function AppShellDemo() {
|
||||
spacing={0}
|
||||
>
|
||||
<Title order={6}>
|
||||
{configVariableToFriendlyName(configVariable.name)}
|
||||
<FormattedMessage
|
||||
id={`admin.config.${camelToKebab(
|
||||
configVariable.key
|
||||
)}`}
|
||||
/>
|
||||
</Title>
|
||||
{configVariable.description.split("\n").length == 1 ? (
|
||||
<Text color="dimmed" size="sm" mb="xs">
|
||||
{configVariable.description}
|
||||
</Text>
|
||||
) : (
|
||||
configVariable.description.split("\n").map((line) => (
|
||||
<Text
|
||||
key={line}
|
||||
color="dimmed"
|
||||
size="sm"
|
||||
style={{
|
||||
marginBottom: line === "" ? "1rem" : "0",
|
||||
}}
|
||||
>
|
||||
{line}
|
||||
</Text>
|
||||
))
|
||||
)}
|
||||
|
||||
<Text
|
||||
sx={{
|
||||
whiteSpace: "pre-line",
|
||||
}}
|
||||
color="dimmed"
|
||||
size="sm"
|
||||
mb="xs"
|
||||
>
|
||||
<FormattedMessage
|
||||
id={`admin.config.${camelToKebab(
|
||||
configVariable.key
|
||||
)}.description`}
|
||||
values={{ br: <br /> }}
|
||||
/>
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack></Stack>
|
||||
<Box style={{ width: isMobile ? "100%" : "50%" }}>
|
||||
@@ -176,7 +181,9 @@ export default function AppShellDemo() {
|
||||
saveConfigVariables={saveConfigVariables}
|
||||
/>
|
||||
)}
|
||||
<Button onClick={saveConfigVariables}>Save</Button>
|
||||
<Button onClick={saveConfigVariables}>
|
||||
<FormattedMessage id="common.button.save" />
|
||||
</Button>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TbRefresh, TbSettings, TbUsers } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../components/Meta";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import configService from "../../services/config.service";
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
@@ -31,15 +33,16 @@ const useStyles = createStyles((theme) => ({
|
||||
|
||||
const Admin = () => {
|
||||
const { classes, theme } = useStyles();
|
||||
const t = useTranslate();
|
||||
|
||||
const [managementOptions, setManagementOptions] = useState([
|
||||
{
|
||||
title: "User management",
|
||||
title: t("admin.button.users"),
|
||||
icon: TbUsers,
|
||||
route: "/admin/users",
|
||||
},
|
||||
{
|
||||
title: "Configuration",
|
||||
title: t("admin.button.config"),
|
||||
icon: TbSettings,
|
||||
route: "/admin/config/general",
|
||||
},
|
||||
@@ -63,9 +66,9 @@ const Admin = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="Administration" />
|
||||
<Meta title={t("admin.title")} />
|
||||
<Title mb={30} order={3}>
|
||||
Administration
|
||||
<FormattedMessage id="admin.title" />
|
||||
</Title>
|
||||
<Stack justify="space-between" style={{ height: "calc(100vh - 180px)" }}>
|
||||
<Paper withBorder p={40}>
|
||||
@@ -91,7 +94,7 @@ const Admin = () => {
|
||||
|
||||
<Center>
|
||||
<Text size="xs" color="dimmed">
|
||||
Version {process.env.VERSION}
|
||||
<FormattedMessage id="admin.version" /> {process.env.VERSION}
|
||||
</Text>
|
||||
</Center>
|
||||
</Stack>
|
||||
|
||||
@@ -2,10 +2,12 @@ import { Button, Group, Space, Text, Title } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TbPlus } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../components/Meta";
|
||||
import ManageUserTable from "../../components/admin/users/ManageUserTable";
|
||||
import showCreateUserModal from "../../components/admin/users/showCreateUserModal";
|
||||
import Meta from "../../components/Meta";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import userService from "../../services/user.service";
|
||||
import User from "../../types/user.type";
|
||||
import toast from "../../utils/toast.util";
|
||||
@@ -16,6 +18,7 @@ const Users = () => {
|
||||
|
||||
const config = useConfig();
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const getUsers = () => {
|
||||
setIsLoading(true);
|
||||
@@ -27,14 +30,18 @@ const Users = () => {
|
||||
|
||||
const deleteUser = (user: User) => {
|
||||
modals.openConfirmModal({
|
||||
title: `Delete ${user.username}?`,
|
||||
title: t("admin.users.edit.delete.title", {
|
||||
username: user.username,
|
||||
}),
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Do you really want to delete <b>{user.username}</b> and all his
|
||||
shares?
|
||||
<FormattedMessage id="admin.users.edit.delete.description" />
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
||||
labels: {
|
||||
confirm: t("common.button.delete"),
|
||||
cancel: t("common.button.cancel"),
|
||||
},
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: async () => {
|
||||
userService
|
||||
@@ -51,10 +58,10 @@ const Users = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="User management" />
|
||||
<Meta title={t("admin.users.title")} />
|
||||
<Group position="apart" align="baseline" mb={20}>
|
||||
<Title mb={30} order={3}>
|
||||
User management
|
||||
<FormattedMessage id="admin.users.title" />
|
||||
</Title>
|
||||
<Button
|
||||
onClick={() =>
|
||||
@@ -62,7 +69,7 @@ const Users = () => {
|
||||
}
|
||||
leftIcon={<TbPlus size={20} />}
|
||||
>
|
||||
Create
|
||||
<FormattedMessage id="common.button.create" />
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import { useRouter } from "next/router";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
import authService from "../../../services/auth.service";
|
||||
import toast from "../../../utils/toast.util";
|
||||
|
||||
@@ -25,6 +27,7 @@ const useStyles = createStyles((theme) => ({
|
||||
const ResetPassword = () => {
|
||||
const { classes } = useStyles();
|
||||
const router = useRouter();
|
||||
const t = useTranslate();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
@@ -32,7 +35,10 @@ const ResetPassword = () => {
|
||||
},
|
||||
validate: yupResolver(
|
||||
yup.object().shape({
|
||||
password: yup.string().min(8).required(),
|
||||
password: yup
|
||||
.string()
|
||||
.min(8, t("common.error.too-short", { length: 8 }))
|
||||
.required(t("common.error.field-required")),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -42,10 +48,10 @@ const ResetPassword = () => {
|
||||
return (
|
||||
<Container size={460} my={30}>
|
||||
<Title order={2} weight={900} align="center">
|
||||
Reset password
|
||||
<FormattedMessage id="resetPassword.text.resetPassword" />
|
||||
</Title>
|
||||
<Text color="dimmed" size="sm" align="center">
|
||||
Enter your new password
|
||||
<FormattedMessage id="resetPassword.text.enterNewPassword" />
|
||||
</Text>
|
||||
|
||||
<Paper withBorder shadow="md" p={30} radius="md" mt="xl">
|
||||
@@ -54,7 +60,7 @@ const ResetPassword = () => {
|
||||
authService
|
||||
.resetPassword(resetPasswordToken, values.password)
|
||||
.then(() => {
|
||||
toast.success("Your password has been reset successfully.");
|
||||
toast.success(t("resetPassword.notify.passwordReset"));
|
||||
|
||||
router.push("/auth/signIn");
|
||||
})
|
||||
@@ -62,13 +68,13 @@ const ResetPassword = () => {
|
||||
})}
|
||||
>
|
||||
<PasswordInput
|
||||
label="New password"
|
||||
label={t("resetPassword.text.password")}
|
||||
placeholder="••••••••••"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<Group position="right" mt="lg">
|
||||
<Button type="submit" className={classes.control}>
|
||||
Reset password
|
||||
<FormattedMessage id="resetPassword.button.resetPassword" />
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
|
||||
@@ -15,7 +15,9 @@ import { useForm, yupResolver } from "@mantine/form";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { TbArrowLeft } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
import authService from "../../../services/auth.service";
|
||||
import toast from "../../../utils/toast.util";
|
||||
|
||||
@@ -43,6 +45,7 @@ const useStyles = createStyles((theme) => ({
|
||||
const ResetPassword = () => {
|
||||
const { classes } = useStyles();
|
||||
const router = useRouter();
|
||||
const t = useTranslate();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
@@ -50,7 +53,10 @@ const ResetPassword = () => {
|
||||
},
|
||||
validate: yupResolver(
|
||||
yup.object().shape({
|
||||
email: yup.string().email().required(),
|
||||
email: yup
|
||||
.string()
|
||||
.email(t("common.error.invalid-email"))
|
||||
.required(t("common.error.field-required")),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -58,10 +64,10 @@ const ResetPassword = () => {
|
||||
return (
|
||||
<Container size={460} my={30}>
|
||||
<Title order={2} weight={900} align="center">
|
||||
Forgot your password?
|
||||
<FormattedMessage id="resetPassword.title" />
|
||||
</Title>
|
||||
<Text color="dimmed" size="sm" align="center">
|
||||
Enter your email to get a reset link
|
||||
<FormattedMessage id="resetPassword.description" />
|
||||
</Text>
|
||||
|
||||
<Paper withBorder shadow="md" p={30} radius="md" mt="xl">
|
||||
@@ -70,15 +76,15 @@ const ResetPassword = () => {
|
||||
authService
|
||||
.requestResetPassword(values.email)
|
||||
.then(() => {
|
||||
toast.success("The email has been sent.");
|
||||
toast.success(t("resetPassword.notify.success"));
|
||||
router.push("/auth/signIn");
|
||||
})
|
||||
.catch(toast.axiosError)
|
||||
)}
|
||||
>
|
||||
<TextInput
|
||||
label="Your email"
|
||||
placeholder="Your email"
|
||||
label={t("signup.input.email")}
|
||||
placeholder={t("signup.input.email.placeholder")}
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
<Group position="apart" mt="lg" className={classes.controls}>
|
||||
@@ -91,11 +97,13 @@ const ResetPassword = () => {
|
||||
>
|
||||
<Center inline>
|
||||
<TbArrowLeft size={12} />
|
||||
<Box ml={5}>Back to login page</Box>
|
||||
<Box ml={5}>
|
||||
<FormattedMessage id="resetPassword.button.back" />
|
||||
</Box>
|
||||
</Center>
|
||||
</Anchor>
|
||||
<Button type="submit" className={classes.control}>
|
||||
Reset password
|
||||
<FormattedMessage id="resetPassword.text.resetPassword" />
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
|
||||
import SignInForm from "../../components/auth/SignInForm";
|
||||
import Meta from "../../components/Meta";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
|
||||
export function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
return {
|
||||
@@ -15,6 +16,7 @@ export function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const SignIn = ({ redirectPath }: { redirectPath?: string }) => {
|
||||
const { refreshUser } = useUser();
|
||||
const router = useRouter();
|
||||
const t = useTranslate();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(redirectPath ? true : false);
|
||||
|
||||
@@ -34,7 +36,7 @@ const SignIn = ({ redirectPath }: { redirectPath?: string }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="Sign In" />
|
||||
<Meta title={t("signin.title")} />
|
||||
<SignInForm redirectPath={redirectPath ?? "/upload"} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import SignUpForm from "../../components/auth/SignUpForm";
|
||||
import Meta from "../../components/Meta";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
|
||||
const SignUp = () => {
|
||||
const t = useTranslate();
|
||||
return (
|
||||
<>
|
||||
<Meta title="Sign Up" />
|
||||
<Meta title={t("signup.title")} />
|
||||
<SignUpForm />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { TbCheck } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Logo from "../components/Logo";
|
||||
import Meta from "../components/Meta";
|
||||
import useUser from "../hooks/user.hook";
|
||||
@@ -89,12 +90,17 @@ export default function Home() {
|
||||
<div className={classes.inner}>
|
||||
<div className={classes.content}>
|
||||
<Title className={classes.title}>
|
||||
A <span className={classes.highlight}>self-hosted</span> <br />{" "}
|
||||
file sharing platform.
|
||||
<FormattedMessage
|
||||
id="home.title"
|
||||
values={{
|
||||
h: (chunks) => (
|
||||
<span className={classes.highlight}>{chunks} </span>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Title>
|
||||
<Text color="dimmed" mt="md">
|
||||
Do you really want to give your personal files in the hand of
|
||||
third parties like WeTransfer?
|
||||
<FormattedMessage id="home.description" />
|
||||
</Text>
|
||||
|
||||
<List
|
||||
@@ -109,19 +115,26 @@ export default function Home() {
|
||||
>
|
||||
<List.Item>
|
||||
<div>
|
||||
<b>Self-Hosted</b> - Host Pingvin Share on your own machine.
|
||||
<b>
|
||||
<FormattedMessage id="home.bullet.a.name" />
|
||||
</b>{" "}
|
||||
- <FormattedMessage id="home.bullet.a.description" />
|
||||
</div>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<div>
|
||||
<b>Privacy</b> - Your files are your files and should never
|
||||
get into the hands of third parties.
|
||||
<b>
|
||||
<FormattedMessage id="home.bullet.b.name" />
|
||||
</b>{" "}
|
||||
- <FormattedMessage id="home.bullet.b.description" />
|
||||
</div>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<div>
|
||||
<b>No annoying file size limit</b> - Upload as big files as
|
||||
you want. Only your hard drive will be your limit.
|
||||
<b>
|
||||
<FormattedMessage id="home.bullet.c.name" />
|
||||
</b>{" "}
|
||||
- <FormattedMessage id="home.bullet.c.description" />
|
||||
</div>
|
||||
</List.Item>
|
||||
</List>
|
||||
@@ -134,7 +147,7 @@ export default function Home() {
|
||||
size="md"
|
||||
className={classes.control}
|
||||
>
|
||||
Get started
|
||||
<FormattedMessage id="home.button.start" />
|
||||
</Button>
|
||||
<Button
|
||||
component={Link}
|
||||
@@ -145,7 +158,7 @@ export default function Home() {
|
||||
size="md"
|
||||
className={classes.control}
|
||||
>
|
||||
Source code
|
||||
<FormattedMessage id="home.button.source" />
|
||||
</Button>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import DownloadAllButton from "../../../components/share/DownloadAllButton";
|
||||
import FileList from "../../../components/share/FileList";
|
||||
import showEnterPasswordModal from "../../../components/share/showEnterPasswordModal";
|
||||
import showErrorModal from "../../../components/share/showErrorModal";
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
import shareService from "../../../services/share.service";
|
||||
import { Share as ShareType } from "../../../types/share.type";
|
||||
import toast from "../../../utils/toast.util";
|
||||
@@ -20,6 +21,7 @@ export function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const Share = ({ shareId }: { shareId: string }) => {
|
||||
const modals = useModals();
|
||||
const [share, setShare] = useState<ShareType>();
|
||||
const t = useTranslate();
|
||||
|
||||
const getShareToken = async (password?: string) => {
|
||||
await shareService
|
||||
@@ -33,8 +35,8 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||
if (error == "share_max_views_exceeded") {
|
||||
showErrorModal(
|
||||
modals,
|
||||
"Visitor limit exceeded",
|
||||
"The visitor limit from this share has been exceeded."
|
||||
t("share.error.visitor-limit-exceeded.title"),
|
||||
t("share.error.visitor-limit-exceeded.description")
|
||||
);
|
||||
} else {
|
||||
toast.axiosError(e);
|
||||
@@ -52,12 +54,16 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||
const { error } = e.response.data;
|
||||
if (e.response.status == 404) {
|
||||
if (error == "share_removed") {
|
||||
showErrorModal(modals, "Share removed", e.response.data.message);
|
||||
showErrorModal(
|
||||
modals,
|
||||
t("share.error.removed.title"),
|
||||
e.response.data.message
|
||||
);
|
||||
} else {
|
||||
showErrorModal(
|
||||
modals,
|
||||
"Not found",
|
||||
"This share can't be found. Please check your link."
|
||||
t("share.error.not-found.title"),
|
||||
t("share.error.not-found.description")
|
||||
);
|
||||
}
|
||||
} else if (error == "share_password_required") {
|
||||
@@ -65,7 +71,7 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||
} else if (error == "share_token_required") {
|
||||
getShareToken();
|
||||
} else {
|
||||
showErrorModal(modals, "Error", "An unknown error occurred.");
|
||||
showErrorModal(modals, t("common.error"), t("common.error.unknown"));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -77,8 +83,8 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
title={`Share ${shareId}`}
|
||||
description="Look what I've shared with you."
|
||||
title={t("share.title", { shareId })}
|
||||
description={t("share.description")}
|
||||
/>
|
||||
|
||||
<Group position="apart" mb="lg">
|
||||
|
||||
@@ -4,12 +4,14 @@ import { cleanNotifications } from "@mantine/notifications";
|
||||
import { AxiosError } from "axios";
|
||||
import pLimit from "p-limit";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../components/Meta";
|
||||
import Dropzone from "../../components/upload/Dropzone";
|
||||
import FileList from "../../components/upload/FileList";
|
||||
import showCompletedUploadModal from "../../components/upload/modals/showCompletedUploadModal";
|
||||
import showCreateUploadModal from "../../components/upload/modals/showCreateUploadModal";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
import shareService from "../../services/share.service";
|
||||
import { FileUpload } from "../../types/File.type";
|
||||
@@ -29,6 +31,7 @@ const Upload = ({
|
||||
isReverseShare: boolean;
|
||||
}) => {
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const { user } = useUser();
|
||||
const config = useConfig();
|
||||
@@ -126,7 +129,7 @@ const Upload = ({
|
||||
if (fileErrorCount > 0) {
|
||||
if (!errorToastShown) {
|
||||
toast.error(
|
||||
`${fileErrorCount} file(s) failed to upload. Trying again.`,
|
||||
t("upload.notify.count-failed", { count: fileErrorCount }),
|
||||
{
|
||||
withCloseButton: false,
|
||||
autoClose: false,
|
||||
@@ -152,15 +155,13 @@ const Upload = ({
|
||||
showCompletedUploadModal(modals, share, config.get("general.appUrl"));
|
||||
setFiles([]);
|
||||
})
|
||||
.catch(() =>
|
||||
toast.error("An error occurred while finishing your share.")
|
||||
);
|
||||
.catch(() => toast.error(t("upload.notify.generic-error")));
|
||||
}
|
||||
}, [files]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="Upload" />
|
||||
<Meta title={t("upload.title")} />
|
||||
<Group position="right" mb={20}>
|
||||
<Button
|
||||
loading={isUploading}
|
||||
@@ -183,7 +184,7 @@ const Upload = ({
|
||||
);
|
||||
}}
|
||||
>
|
||||
Share
|
||||
<FormattedMessage id="common.button.share" />
|
||||
</Button>
|
||||
</Group>
|
||||
<Dropzone
|
||||
|
||||
Reference in New Issue
Block a user