mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: improve i18n support as a whole (#1526)
* feat: improve i18n support as a whole - Remove dayjs in favor of /helpers/datetime.ts, which uses Intl.DateTimeFormat and Date. Dayjs is not exactly i18n friendly and has several locale related opened issues. - Move/refactor date/time code from /helpers/utils.ts to /helpers/datetime.ts. - Fix Daily Review weekday not changing according to selected date. - Localize Daily review weekday and month. - Load i18n listed strings from /locales/{locale}.json in a dynamic way. This makes much easier to add new locales, by just adding a properly named json file and listing it only in /web/src/i18n.ts and /api/user_setting.go. - Fallback languages are now set in /web/src/i18n.ts. - Full language codes are now preffered, but they fallback to 2-letter codes when not available. - The locale dropdown is now populated dynamically from the available locales. Locale names are populated by the browser via Intl.DisplayNames(locale). - /web/src/i18n.ts now exports a type TLocale from availableLocales array. This is used only by findNearestLanguageMatch(). As I was unable to use this type in ".d.ts" files, I switched the Locale type from /web/src/types/i18n.d.ts to string. - Move pretty much all hardcoded text strings to i18n strings. - Add pt-BR translation. - Remove site.ts and move its content to a i18n string. - Rename zh.json to zh-Hans.json to get the correct language name on selector dropdown. - Remove pt_BR.json and replace with pt-BR.json. - Some minor layout spacing fixes to accommodate larger texts. - Improve some error messages. * Delete .yarnrc.yml * Delete package-lock.json * fix: 158:28 error Insert `⏎` prettier/prettier
This commit is contained in:
@ -32,7 +32,25 @@ func (key UserSettingKey) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
UserSettingLocaleValue = []string{"en", "zh", "vi", "fr", "nl", "sv", "de", "es", "uk", "ru", "it", "hant", "tr", "ko", "sl"}
|
UserSettingLocaleValue = []string{
|
||||||
|
"de",
|
||||||
|
"en",
|
||||||
|
"es",
|
||||||
|
"fr",
|
||||||
|
"it",
|
||||||
|
"ko",
|
||||||
|
"nl",
|
||||||
|
"pl",
|
||||||
|
"pt-BR",
|
||||||
|
"ru",
|
||||||
|
"sl",
|
||||||
|
"sv",
|
||||||
|
"tr",
|
||||||
|
"uk",
|
||||||
|
"vi",
|
||||||
|
"zh-Hans",
|
||||||
|
"zh-Hant",
|
||||||
|
}
|
||||||
UserSettingAppearanceValue = []string{"system", "light", "dark"}
|
UserSettingAppearanceValue = []string{"system", "light", "dark"}
|
||||||
UserSettingMemoVisibilityValue = []Visibility{Private, Protected, Public}
|
UserSettingMemoVisibilityValue = []Visibility{Private, Protected, Public}
|
||||||
)
|
)
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
"@reduxjs/toolkit": "^1.8.1",
|
"@reduxjs/toolkit": "^1.8.1",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"copy-to-clipboard": "^3.3.2",
|
"copy-to-clipboard": "^3.3.2",
|
||||||
"dayjs": "^1.11.3",
|
|
||||||
"highlight.js": "^11.6.0",
|
"highlight.js": "^11.6.0",
|
||||||
"i18next": "^21.9.2",
|
"i18next": "^21.9.2",
|
||||||
"i18next-browser-languagedetector": "^7.0.1",
|
"i18next-browser-languagedetector": "^7.0.1",
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import { useColorScheme } from "@mui/joy";
|
import { useColorScheme } from "@mui/joy";
|
||||||
import { useEffect, Suspense } from "react";
|
import { useEffect, Suspense } from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
@ -59,7 +58,6 @@ const App = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.setAttribute("lang", locale);
|
document.documentElement.setAttribute("lang", locale);
|
||||||
i18n.changeLanguage(locale);
|
i18n.changeLanguage(locale);
|
||||||
dayjs.locale(locale);
|
|
||||||
storage.set({
|
storage.set({
|
||||||
locale: locale,
|
locale: locale,
|
||||||
});
|
});
|
||||||
|
@ -27,10 +27,11 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col justify-start items-start max-w-full w-96">
|
<div className="flex flex-col justify-start items-start max-w-full w-96">
|
||||||
<p className="text-sm">{customizedProfile.description || "No description"}</p>
|
<p className="text-xs">{t("about.memos-description")}</p>
|
||||||
|
<p className="text-sm mt-2 ">{customizedProfile.description || t("about.no-server-description")}</p>
|
||||||
<div className="mt-4 w-full flex flex-row text-sm justify-start items-center">
|
<div className="mt-4 w-full flex flex-row text-sm justify-start items-center">
|
||||||
<div className="flex flex-row justify-start items-center mr-2">
|
<div className="flex flex-row justify-start items-center mr-2">
|
||||||
Powered by
|
{t("about.powered-by")}
|
||||||
<a href="https://usememos.com" target="_blank" className="flex flex-row justify-start items-center mx-1 hover:underline">
|
<a href="https://usememos.com" target="_blank" className="flex flex-row justify-start items-center mx-1 hover:underline">
|
||||||
<img className="w-6 h-auto rounded-full mr-1" src="/logo.webp" alt="" />
|
<img className="w-6 h-auto rounded-full mr-1" src="/logo.webp" alt="" />
|
||||||
memos
|
memos
|
||||||
@ -40,7 +41,7 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
<GitHubBadge />
|
<GitHubBadge />
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t w-full mt-3 pt-2 text-sm flex flex-row justify-start items-center space-x-2">
|
<div className="border-t w-full mt-3 pt-2 text-sm flex flex-row justify-start items-center space-x-2">
|
||||||
<span className="text-gray-500">Other projects:</span>
|
<span className="text-gray-500">{t("about.other-projects")}:</span>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/boojack/sticky-notes"
|
href="https://github.com/boojack/sticky-notes"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMemoStore } from "@/store/module";
|
import { useMemoStore } from "@/store/module";
|
||||||
import * as utils from "@/helpers/utils";
|
import { getDateTimeString } from "@/helpers/datetime";
|
||||||
import useToggle from "@/hooks/useToggle";
|
import useToggle from "@/hooks/useToggle";
|
||||||
import MemoContent from "./MemoContent";
|
import MemoContent from "./MemoContent";
|
||||||
import MemoResources from "./MemoResources";
|
import MemoResources from "./MemoResources";
|
||||||
@ -54,7 +54,7 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
|
|||||||
<div className={`memo-wrapper archived ${"memos-" + memo.id}`} onMouseLeave={handleMouseLeaveMemoWrapper}>
|
<div className={`memo-wrapper archived ${"memos-" + memo.id}`} onMouseLeave={handleMouseLeaveMemoWrapper}>
|
||||||
<div className="memo-top-wrapper">
|
<div className="memo-top-wrapper">
|
||||||
<span className="time-text">
|
<span className="time-text">
|
||||||
{t("memo.archived-at")} {utils.getDateTimeString(memo.updatedTs)}
|
{t("memo.archived-at")} {getDateTimeString(memo.updatedTs)}
|
||||||
</span>
|
</span>
|
||||||
<div className="btns-container">
|
<div className="btns-container">
|
||||||
<span className="btn-text" onClick={handleRestoreMemoClick}>
|
<span className="btn-text" onClick={handleRestoreMemoClick}>
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BetaBadge: React.FC<Props> = (props: Props) => {
|
const BetaBadge: React.FC<Props> = (props: Props) => {
|
||||||
const { className } = props;
|
const { className } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`mx-1 px-1 leading-5 text-xs border dark:border-zinc-600 rounded-full text-gray-500 dark:text-gray-400 ${className ?? ""}`}
|
className={`mx-1 px-1 leading-5 text-xs border dark:border-zinc-600 rounded-full text-gray-500 dark:text-gray-400 ${className ?? ""}`}
|
||||||
>
|
>
|
||||||
Beta
|
{t("common.beta")}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -74,7 +74,7 @@ const ChangeMemberPasswordDialog: React.FC<Props> = (props: Props) => {
|
|||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
className="input-text"
|
className="input-text"
|
||||||
placeholder={t("auth.repeat-new-password")}
|
placeholder={t("auth.new-password")}
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={handleNewPasswordChanged}
|
onChange={handleNewPasswordChanged}
|
||||||
/>
|
/>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import dayjs from "dayjs";
|
import { getNormalizedTimeString, getUnixTime } from "@/helpers/datetime";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -15,12 +15,12 @@ const ChangeMemoCreatedTsDialog: React.FC<Props> = (props: Props) => {
|
|||||||
const { destroy, memoId } = props;
|
const { destroy, memoId } = props;
|
||||||
const memoStore = useMemoStore();
|
const memoStore = useMemoStore();
|
||||||
const [createdAt, setCreatedAt] = useState("");
|
const [createdAt, setCreatedAt] = useState("");
|
||||||
const maxDatetimeValue = dayjs().format("YYYY-MM-DDTHH:mm");
|
const maxDatetimeValue = getNormalizedTimeString();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
memoStore.getMemoById(memoId).then((memo) => {
|
memoStore.getMemoById(memoId).then((memo) => {
|
||||||
if (memo) {
|
if (memo) {
|
||||||
const datetime = dayjs(memo.createdTs).format("YYYY-MM-DDTHH:mm");
|
const datetime = getNormalizedTimeString(memo.createdTs);
|
||||||
setCreatedAt(datetime);
|
setCreatedAt(datetime);
|
||||||
} else {
|
} else {
|
||||||
toast.error(t("message.memo-not-found"));
|
toast.error(t("message.memo-not-found"));
|
||||||
@ -39,8 +39,8 @@ const ChangeMemoCreatedTsDialog: React.FC<Props> = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveBtnClick = async () => {
|
const handleSaveBtnClick = async () => {
|
||||||
const nowTs = dayjs().unix();
|
const nowTs = getUnixTime();
|
||||||
const createdTs = dayjs(createdAt).unix();
|
const createdTs = getUnixTime(createdAt);
|
||||||
|
|
||||||
if (createdTs > nowTs) {
|
if (createdTs > nowTs) {
|
||||||
toast.error(t("message.invalid-created-datetime"));
|
toast.error(t("message.invalid-created-datetime"));
|
||||||
@ -69,9 +69,10 @@ const ChangeMemoCreatedTsDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col justify-start items-start !w-72 max-w-full">
|
<div className="flex flex-col justify-start items-start !w-72 max-w-full">
|
||||||
<p className="w-full bg-yellow-100 border border-yellow-400 rounded p-2 text-xs leading-4">
|
<div className="w-full bg-yellow-100 border border-yellow-400 rounded p-2 text-black">
|
||||||
THIS IS NOT A NORMAL BEHAVIOR. PLEASE MAKE SURE YOU REALLY NEED IT.
|
<p className="uppercase">{t("message.change-memo-created-time-warning-1")}</p>
|
||||||
</p>
|
<p>{t("message.change-memo-created-time-warning-2")}</p>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
className="input-text mt-2"
|
className="input-text mt-2"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
|
@ -71,7 +71,7 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
type="password"
|
type="password"
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
className="input-text"
|
className="input-text"
|
||||||
placeholder={t("auth.repeat-new-password")}
|
placeholder={t("auth.new-password")}
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={handleNewPasswordChanged}
|
onChange={handleNewPasswordChanged}
|
||||||
/>
|
/>
|
||||||
|
@ -6,13 +6,16 @@ import { UNKNOWN_ID } from "@/helpers/consts";
|
|||||||
import { absolutifyLink } from "@/helpers/utils";
|
import { absolutifyLink } from "@/helpers/utils";
|
||||||
import { generateDialog } from "./Dialog";
|
import { generateDialog } from "./Dialog";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props extends DialogProps {
|
interface Props extends DialogProps {
|
||||||
identityProvider?: IdentityProvider;
|
identityProvider?: IdentityProvider;
|
||||||
confirmCallback?: () => void;
|
confirmCallback?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const templateList: IdentityProvider[] = [
|
const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const templateList: IdentityProvider[] = [
|
||||||
{
|
{
|
||||||
id: UNKNOWN_ID,
|
id: UNKNOWN_ID,
|
||||||
name: "GitHub",
|
name: "GitHub",
|
||||||
@ -27,9 +30,9 @@ const templateList: IdentityProvider[] = [
|
|||||||
userInfoUrl: "https://api.github.com/user",
|
userInfoUrl: "https://api.github.com/user",
|
||||||
scopes: ["user"],
|
scopes: ["user"],
|
||||||
fieldMapping: {
|
fieldMapping: {
|
||||||
identifier: "login",
|
identifier: t("setting.sso-section.identifier"),
|
||||||
displayName: "name",
|
displayName: "",
|
||||||
email: "email",
|
email: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -48,9 +51,9 @@ const templateList: IdentityProvider[] = [
|
|||||||
userInfoUrl: "https://gitlab.com/oauth/userinfo",
|
userInfoUrl: "https://gitlab.com/oauth/userinfo",
|
||||||
scopes: ["openid"],
|
scopes: ["openid"],
|
||||||
fieldMapping: {
|
fieldMapping: {
|
||||||
identifier: "name",
|
identifier: t("setting.sso-section.identifier"),
|
||||||
displayName: "name",
|
displayName: "",
|
||||||
email: "email",
|
email: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -69,16 +72,16 @@ const templateList: IdentityProvider[] = [
|
|||||||
userInfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
|
userInfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
|
||||||
scopes: ["https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"],
|
scopes: ["https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"],
|
||||||
fieldMapping: {
|
fieldMapping: {
|
||||||
identifier: "email",
|
identifier: t("setting.sso-section.identifier"),
|
||||||
displayName: "name",
|
displayName: "",
|
||||||
email: "email",
|
email: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: UNKNOWN_ID,
|
id: UNKNOWN_ID,
|
||||||
name: "Custom",
|
name: t("setting.sso-section.custom"),
|
||||||
type: "OAUTH2",
|
type: "OAUTH2",
|
||||||
identifierFilter: "",
|
identifierFilter: "",
|
||||||
config: {
|
config: {
|
||||||
@ -97,9 +100,7 @@ const templateList: IdentityProvider[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
|
|
||||||
const { confirmCallback, destroy, identityProvider } = props;
|
const { confirmCallback, destroy, identityProvider } = props;
|
||||||
const [basicInfo, setBasicInfo] = useState({
|
const [basicInfo, setBasicInfo] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
@ -193,7 +194,7 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
toast.success(`SSO ${basicInfo.name} created`);
|
toast.success(t("setting.sso-section.sso-created", { name: basicInfo.name }));
|
||||||
} else {
|
} else {
|
||||||
await api.patchIdentityProvider({
|
await api.patchIdentityProvider({
|
||||||
id: identityProvider?.id,
|
id: identityProvider?.id,
|
||||||
@ -206,7 +207,7 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
toast.success(`SSO ${basicInfo.name} updated`);
|
toast.success(t("setting.sso-section.sso-updated", { name: basicInfo.name }));
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -228,16 +229,16 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dialog-header-container">
|
<div className="dialog-header-container">
|
||||||
<p className="title-text">{isCreating ? "Create SSO" : "Update SSO"}</p>
|
<p className="title-text">{t("setting.sso-section." + (isCreating ? "create" : "update") + "-sso")}</p>
|
||||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||||
<Icon.X />
|
<Icon.X />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container w-full max-w-[24rem]">
|
<div className="dialog-content-container w-full max-w-[24rem] min-w-[25rem]">
|
||||||
{isCreating && (
|
{isCreating && (
|
||||||
<>
|
<>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
Type
|
{t("common.type")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<RadioGroup className="mb-2" value={type}>
|
<RadioGroup className="mb-2" value={type}>
|
||||||
<div className="mt-2 w-full flex flex-row space-x-4">
|
<div className="mt-2 w-full flex flex-row space-x-4">
|
||||||
@ -245,7 +246,7 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
<Typography className="mb-2" level="body2">
|
<Typography className="mb-2" level="body2">
|
||||||
Template
|
{t("setting.sso-section.template")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<RadioGroup className="mb-2" value={seletedTemplate}>
|
<RadioGroup className="mb-2" value={seletedTemplate}>
|
||||||
<div className="mt-2 w-full flex flex-row space-x-4">
|
<div className="mt-2 w-full flex flex-row space-x-4">
|
||||||
@ -263,11 +264,12 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
Name<span className="text-red-600">*</span>
|
{t("common.name")}
|
||||||
|
<span className="text-red-600">*</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="Name"
|
placeholder={t("common.name")}
|
||||||
value={basicInfo.name}
|
value={basicInfo.name}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setBasicInfo({
|
setBasicInfo({
|
||||||
@ -278,11 +280,11 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
Identifier filter
|
{t("setting.sso-section.identifier-filter")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="Identifier filter"
|
placeholder={t("setting.sso-section.identifier-filter")}
|
||||||
value={basicInfo.identifierFilter}
|
value={basicInfo.identifierFilter}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setBasicInfo({
|
setBasicInfo({
|
||||||
@ -296,89 +298,104 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
|
|||||||
{type === "OAUTH2" && (
|
{type === "OAUTH2" && (
|
||||||
<>
|
<>
|
||||||
{isCreating && (
|
{isCreating && (
|
||||||
<p className="border rounded-md p-2 text-sm w-full mb-2 break-all">Redirect URL: {absolutifyLink("/auth/callback")}</p>
|
<p className="border rounded-md p-2 text-sm w-full mb-2 break-all">
|
||||||
|
{t("setting.sso-section.redirect-url")}: {absolutifyLink("/auth/callback")}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
Client ID<span className="text-red-600">*</span>
|
{t("setting.sso-section.client-id")}
|
||||||
|
<span className="text-red-600">*</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="Client ID"
|
placeholder={t("setting.sso-section.client-id")}
|
||||||
value={oauth2Config.clientId}
|
value={oauth2Config.clientId}
|
||||||
onChange={(e) => setPartialOAuth2Config({ clientId: e.target.value })}
|
onChange={(e) => setPartialOAuth2Config({ clientId: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
Client secret<span className="text-red-600">*</span>
|
{t("setting.sso-section.client-secret")}
|
||||||
|
<span className="text-red-600">*</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="Client secret"
|
placeholder={t("setting.sso-section.client-secret")}
|
||||||
value={oauth2Config.clientSecret}
|
value={oauth2Config.clientSecret}
|
||||||
onChange={(e) => setPartialOAuth2Config({ clientSecret: e.target.value })}
|
onChange={(e) => setPartialOAuth2Config({ clientSecret: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
Authorization endpoint<span className="text-red-600">*</span>
|
{t("setting.sso-section.authorization-endpoint")}
|
||||||
|
<span className="text-red-600">*</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="Authorization endpoint"
|
placeholder={t("setting.sso-section.authorization-endpoint")}
|
||||||
value={oauth2Config.authUrl}
|
value={oauth2Config.authUrl}
|
||||||
onChange={(e) => setPartialOAuth2Config({ authUrl: e.target.value })}
|
onChange={(e) => setPartialOAuth2Config({ authUrl: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
Token endpoint<span className="text-red-600">*</span>
|
{t("setting.sso-section.token-endpoint")}
|
||||||
|
<span className="text-red-600">*</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="Token endpoint"
|
placeholder={t("setting.sso-section.token-endpoint")}
|
||||||
value={oauth2Config.tokenUrl}
|
value={oauth2Config.tokenUrl}
|
||||||
onChange={(e) => setPartialOAuth2Config({ tokenUrl: e.target.value })}
|
onChange={(e) => setPartialOAuth2Config({ tokenUrl: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
User info endpoint<span className="text-red-600">*</span>
|
{t("setting.sso-section.user-endpoint")}
|
||||||
|
<span className="text-red-600">*</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="User info endpoint"
|
placeholder={t("setting.sso-section.user-endpoint")}
|
||||||
value={oauth2Config.userInfoUrl}
|
value={oauth2Config.userInfoUrl}
|
||||||
onChange={(e) => setPartialOAuth2Config({ userInfoUrl: e.target.value })}
|
onChange={(e) => setPartialOAuth2Config({ userInfoUrl: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
Scopes<span className="text-red-600">*</span>
|
{t("setting.sso-section.scopes")}
|
||||||
</Typography>
|
<span className="text-red-600">*</span>
|
||||||
<Input className="mb-2" placeholder="Scopes" value={oauth2Scopes} onChange={(e) => setOAuth2Scopes(e.target.value)} fullWidth />
|
|
||||||
<Divider className="!my-2" />
|
|
||||||
<Typography className="!mb-1" level="body2">
|
|
||||||
Identifier<span className="text-red-600">*</span>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="User ID key"
|
placeholder={t("setting.sso-section.scopes")}
|
||||||
|
value={oauth2Scopes}
|
||||||
|
onChange={(e) => setOAuth2Scopes(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<Divider className="!my-2" />
|
||||||
|
<Typography className="!mb-1" level="body2">
|
||||||
|
{t("setting.sso-section.identifier")}
|
||||||
|
<span className="text-red-600">*</span>
|
||||||
|
</Typography>
|
||||||
|
<Input
|
||||||
|
className="mb-2"
|
||||||
|
placeholder={t("setting.sso-section.identifier")}
|
||||||
value={oauth2Config.fieldMapping.identifier}
|
value={oauth2Config.fieldMapping.identifier}
|
||||||
onChange={(e) => setPartialOAuth2Config({ fieldMapping: { ...oauth2Config.fieldMapping, identifier: e.target.value } })}
|
onChange={(e) => setPartialOAuth2Config({ fieldMapping: { ...oauth2Config.fieldMapping, identifier: e.target.value } })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
Display name
|
{t("setting.sso-section.display-name")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="User name key"
|
placeholder={t("setting.sso-section.display-name")}
|
||||||
value={oauth2Config.fieldMapping.displayName}
|
value={oauth2Config.fieldMapping.displayName}
|
||||||
onChange={(e) => setPartialOAuth2Config({ fieldMapping: { ...oauth2Config.fieldMapping, displayName: e.target.value } })}
|
onChange={(e) => setPartialOAuth2Config({ fieldMapping: { ...oauth2Config.fieldMapping, displayName: e.target.value } })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
Email
|
{t("common.email")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="User email key"
|
placeholder={t("common.email")}
|
||||||
value={oauth2Config.fieldMapping.email}
|
value={oauth2Config.fieldMapping.email}
|
||||||
onChange={(e) => setPartialOAuth2Config({ fieldMapping: { ...oauth2Config.fieldMapping, email: e.target.value } })}
|
onChange={(e) => setPartialOAuth2Config({ fieldMapping: { ...oauth2Config.fieldMapping, email: e.target.value } })}
|
||||||
fullWidth
|
fullWidth
|
||||||
@ -387,10 +404,10 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
|
|||||||
)}
|
)}
|
||||||
<div className="mt-2 w-full flex flex-row justify-end items-center space-x-1">
|
<div className="mt-2 w-full flex flex-row justify-end items-center space-x-1">
|
||||||
<Button variant="plain" color="neutral" onClick={handleCloseBtnClick}>
|
<Button variant="plain" color="neutral" onClick={handleCloseBtnClick}>
|
||||||
Cancel
|
{t("common.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleConfirmBtnClick} disabled={!allowConfirmAction()}>
|
<Button onClick={handleConfirmBtnClick} disabled={!allowConfirmAction()}>
|
||||||
{isCreating ? "Create" : "Update"}
|
{t("common." + (isCreating ? "create" : "update"))}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -200,7 +200,7 @@ const CreateResourceDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="https://the.link.to/your/resource"
|
placeholder={t("resource.create-dialog.external-link.link-placeholder")}
|
||||||
value={resourceCreate.externalLink}
|
value={resourceCreate.externalLink}
|
||||||
onChange={handleExternalLinkChanged}
|
onChange={handleExternalLinkChanged}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useShortcutStore, useTagStore } from "@/store/module";
|
import { useShortcutStore, useTagStore } from "@/store/module";
|
||||||
import { filterConsts, getDefaultFilter, relationConsts } from "@/helpers/filter";
|
import { filterConsts, getDefaultFilter, relationConsts } from "@/helpers/filter";
|
||||||
|
import { getNormalizedTimeString } from "@/helpers/datetime";
|
||||||
import useLoading from "@/hooks/useLoading";
|
import useLoading from "@/hooks/useLoading";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import { generateDialog } from "./Dialog";
|
import { generateDialog } from "./Dialog";
|
||||||
@ -166,6 +166,7 @@ const MemoFilterInputer: React.FC<MemoFilterInputerProps> = (props: MemoFilterIn
|
|||||||
|
|
||||||
const typeDataSource = Object.values(filterConsts).map(({ text, value }) => ({ text: t(text), value }));
|
const typeDataSource = Object.values(filterConsts).map(({ text, value }) => ({ text: t(text), value }));
|
||||||
const operatorDataSource = Object.values(filterConsts[type as FilterType].operators).map(({ text, value }) => ({ text: t(text), value }));
|
const operatorDataSource = Object.values(filterConsts[type as FilterType].operators).map(({ text, value }) => ({ text: t(text), value }));
|
||||||
|
const relationDataSource = Object.values(relationConsts).map(({ text, value }) => ({ text: t(text), value }));
|
||||||
|
|
||||||
const valueDataSource =
|
const valueDataSource =
|
||||||
type === "TYPE"
|
type === "TYPE"
|
||||||
@ -176,11 +177,11 @@ const MemoFilterInputer: React.FC<MemoFilterInputerProps> = (props: MemoFilterIn
|
|||||||
return { text: t, value: t };
|
return { text: t, value: t };
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxDatetimeValue = dayjs().format("9999-12-31T23:59");
|
const maxDatetimeValue = getNormalizedTimeString("9999-12-31T23:59");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (type === "DISPLAY_TIME") {
|
if (type === "DISPLAY_TIME") {
|
||||||
const nowDatetimeValue = dayjs().format("YYYY-MM-DDTHH:mm");
|
const nowDatetimeValue = getNormalizedTimeString();
|
||||||
handleValueChange(nowDatetimeValue);
|
handleValueChange(nowDatetimeValue);
|
||||||
} else {
|
} else {
|
||||||
setValue(filter.value.value);
|
setValue(filter.value.value);
|
||||||
@ -240,7 +241,7 @@ const MemoFilterInputer: React.FC<MemoFilterInputerProps> = (props: MemoFilterIn
|
|||||||
{index > 0 ? (
|
{index > 0 ? (
|
||||||
<Selector
|
<Selector
|
||||||
className="relation-selector"
|
className="relation-selector"
|
||||||
dataSource={relationConsts}
|
dataSource={relationDataSource}
|
||||||
value={filter.relation}
|
value={filter.relation}
|
||||||
handleValueChanged={handleRelationChange}
|
handleValueChanged={handleRelationChange}
|
||||||
/>
|
/>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Button, Input, Typography } from "@mui/joy";
|
import { Button, Input, Typography } from "@mui/joy";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import * as api from "@/helpers/api";
|
import * as api from "@/helpers/api";
|
||||||
import { generateDialog } from "./Dialog";
|
import { generateDialog } from "./Dialog";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
@ -13,6 +14,7 @@ interface Props extends DialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CreateStorageServiceDialog: React.FC<Props> = (props: Props) => {
|
const CreateStorageServiceDialog: React.FC<Props> = (props: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { destroy, storage, confirmCallback } = props;
|
const { destroy, storage, confirmCallback } = props;
|
||||||
const [basicInfo, setBasicInfo] = useState({
|
const [basicInfo, setBasicInfo] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
@ -105,7 +107,7 @@ const CreateStorageServiceDialog: React.FC<Props> = (props: Props) => {
|
|||||||
<>
|
<>
|
||||||
<div className="dialog-header-container">
|
<div className="dialog-header-container">
|
||||||
<p className="title-text">
|
<p className="title-text">
|
||||||
{isCreating ? "Create storage" : "Update storage"}
|
{t("setting.storage-section." + (isCreating ? "create" : "update") + "-storage")}
|
||||||
<LearnMore className="ml-2" url="https://usememos.com/docs/storage" />
|
<LearnMore className="ml-2" url="https://usememos.com/docs/storage" />
|
||||||
</p>
|
</p>
|
||||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||||
@ -114,12 +116,12 @@ const CreateStorageServiceDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container">
|
<div className="dialog-content-container">
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
Name
|
{t("common.name")}
|
||||||
<RequiredBadge />
|
<RequiredBadge />
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="Name"
|
placeholder={t("common.name")}
|
||||||
value={basicInfo.name}
|
value={basicInfo.name}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setBasicInfo({
|
setBasicInfo({
|
||||||
@ -130,109 +132,100 @@ const CreateStorageServiceDialog: React.FC<Props> = (props: Props) => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
EndPoint
|
{t("setting.storage-section.endpoint")}
|
||||||
<RequiredBadge />
|
<RequiredBadge />
|
||||||
<span className="text-sm text-gray-400 ml-1">(S3-compatible server URL)</span>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="EndPoint"
|
placeholder={t("setting.storage-section.s3-compatible-url")}
|
||||||
value={s3Config.endPoint}
|
value={s3Config.endPoint}
|
||||||
onChange={(e) => setPartialS3Config({ endPoint: e.target.value })}
|
onChange={(e) => setPartialS3Config({ endPoint: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
Region
|
{t("setting.storage-section.region")}
|
||||||
<RequiredBadge />
|
<RequiredBadge />
|
||||||
<span className="text-sm text-gray-400 ml-1">(Region name)</span>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="Region"
|
placeholder={t("setting.storage-section.region-placeholder")}
|
||||||
value={s3Config.region}
|
value={s3Config.region}
|
||||||
onChange={(e) => setPartialS3Config({ region: e.target.value })}
|
onChange={(e) => setPartialS3Config({ region: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
AccessKey
|
{t("setting.storage-section.accesskey")}
|
||||||
<RequiredBadge />
|
<RequiredBadge />
|
||||||
<span className="text-sm text-gray-400 ml-1">(Access Key / Access ID)</span>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="AccessKey"
|
placeholder={t("setting.storage-section.accesskey-placeholder")}
|
||||||
value={s3Config.accessKey}
|
value={s3Config.accessKey}
|
||||||
onChange={(e) => setPartialS3Config({ accessKey: e.target.value })}
|
onChange={(e) => setPartialS3Config({ accessKey: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
SecretKey
|
{t("setting.storage-section.secretkey")}
|
||||||
<RequiredBadge />
|
<RequiredBadge />
|
||||||
<span className="text-sm text-gray-400 ml-1">(Secret Key / Secret Access Key)</span>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="SecretKey"
|
placeholder={t("setting.storage-section.secretkey-placeholder")}
|
||||||
value={s3Config.secretKey}
|
value={s3Config.secretKey}
|
||||||
onChange={(e) => setPartialS3Config({ secretKey: e.target.value })}
|
onChange={(e) => setPartialS3Config({ secretKey: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
Bucket
|
{t("setting.storage-section.bucket")}
|
||||||
<RequiredBadge />
|
<RequiredBadge />
|
||||||
<span className="text-sm text-gray-400 ml-1">(Bucket name)</span>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="Bucket"
|
placeholder={t("setting.storage-section.bucket-placeholder")}
|
||||||
value={s3Config.bucket}
|
value={s3Config.bucket}
|
||||||
onChange={(e) => setPartialS3Config({ bucket: e.target.value })}
|
onChange={(e) => setPartialS3Config({ bucket: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
Path
|
{t("setting.storage-section.path")}
|
||||||
<span className="text-sm text-gray-400 ml-1">(Storage Path)</span>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
<p className="text-sm text-gray-400 ml-1">{"You can use {year}, {month}, {day}, {hour}, {minute}, {second},"}</p>
|
<p className="text-sm text-gray-400 ml-1">{t("setting.storage-section.path-description")}</p>
|
||||||
<p className="text-sm text-gray-400 ml-1">{"{filename}, {timestamp} and any other words."}</p>
|
<LearnMore className="ml-2" url="https://usememos.com/docs/local-storage" />
|
||||||
<p className="text-sm text-gray-400 ml-1">{"e.g., {year}/{month}/{day}/your/path/{filename}"}</p>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="Path"
|
placeholder={t("setting.storage-section.path-placeholder") + "/{year}/{month}/{filename}"}
|
||||||
value={s3Config.path}
|
value={s3Config.path}
|
||||||
onChange={(e) => setPartialS3Config({ path: e.target.value })}
|
onChange={(e) => setPartialS3Config({ path: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
URLPrefix
|
{t("setting.storage-section.url-prefix")}
|
||||||
<span className="text-sm text-gray-400 ml-1">(Custom URL prefix; Optional)</span>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="URLPrefix"
|
placeholder={t("setting.storage-section.url-prefix-placeholder")}
|
||||||
value={s3Config.urlPrefix}
|
value={s3Config.urlPrefix}
|
||||||
onChange={(e) => setPartialS3Config({ urlPrefix: e.target.value })}
|
onChange={(e) => setPartialS3Config({ urlPrefix: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Typography className="!mb-1" level="body2">
|
<Typography className="!mb-1" level="body2">
|
||||||
URLSuffix
|
{t("setting.storage-section.url-suffix")}
|
||||||
<span className="text-sm text-gray-400 ml-1">(Custom URL suffix; Optional)</span>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="URLSuffix"
|
placeholder={t("setting.storage-section.url-suffix-placeholder")}
|
||||||
value={s3Config.urlSuffix}
|
value={s3Config.urlSuffix}
|
||||||
onChange={(e) => setPartialS3Config({ urlSuffix: e.target.value })}
|
onChange={(e) => setPartialS3Config({ urlSuffix: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<div className="mt-2 w-full flex flex-row justify-end items-center space-x-1">
|
<div className="mt-2 w-full flex flex-row justify-end items-center space-x-1">
|
||||||
<Button variant="plain" color="neutral" onClick={handleCloseBtnClick}>
|
<Button variant="plain" color="neutral" onClick={handleCloseBtnClick}>
|
||||||
Cancel
|
{t("common.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleConfirmBtnClick} disabled={!allowConfirmAction()}>
|
<Button onClick={handleConfirmBtnClick} disabled={!allowConfirmAction()}>
|
||||||
{isCreating ? "Create" : "Update"}
|
{t("common." + (isCreating ? "create" : "update"))}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import * as utils from "@/helpers/utils";
|
import { getTimeString } from "@/helpers/datetime";
|
||||||
import MemoContent from "./MemoContent";
|
import MemoContent from "./MemoContent";
|
||||||
import MemoResources from "./MemoResources";
|
import MemoResources from "./MemoResources";
|
||||||
import "@/less/daily-memo.less";
|
import "@/less/daily-memo.less";
|
||||||
@ -9,7 +9,7 @@ interface Props {
|
|||||||
|
|
||||||
const DailyMemo: React.FC<Props> = (props: Props) => {
|
const DailyMemo: React.FC<Props> = (props: Props) => {
|
||||||
const { memo } = props;
|
const { memo } = props;
|
||||||
const createdTimeStr = utils.getTimeString(memo.createdTs);
|
const createdTimeStr = getTimeString(memo.createdTs);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="daily-memo-wrapper">
|
<div className="daily-memo-wrapper">
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -7,10 +8,11 @@ interface Props {
|
|||||||
|
|
||||||
const LearnMore = (props: Props) => {
|
const LearnMore = (props: Props) => {
|
||||||
const { url, className } = props;
|
const { url, className } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a className={`${className || ""} text-sm text-blue-600 hover:opacity-80 hover:underline`} href={url} target="_blank">
|
<a className={`${className || ""} text-sm text-blue-600 hover:opacity-80 hover:underline`} href={url} target="_blank">
|
||||||
Learn more
|
{t("common.learn-more")}
|
||||||
<Icon.ExternalLink className="inline -mt-1 ml-1 w-4 h-auto opacity-80" />
|
<Icon.ExternalLink className="inline -mt-1 ml-1 w-4 h-auto opacity-80" />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Option, Select } from "@mui/joy";
|
import { Option, Select } from "@mui/joy";
|
||||||
|
import { availableLocales } from "@/i18n";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
|
||||||
@ -17,26 +18,26 @@ const LocaleSelect: FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
className={`!min-w-[10rem] w-auto whitespace-nowrap ${className ?? ""}`}
|
className={`!min-w-[12rem] w-auto whitespace-nowrap ${className ?? ""}`}
|
||||||
startDecorator={<Icon.Globe className="w-4 h-auto" />}
|
startDecorator={<Icon.Globe className="w-4 h-auto" />}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(_, value) => handleSelectChange(value as Locale)}
|
onChange={(_, value) => handleSelectChange(value as Locale)}
|
||||||
>
|
>
|
||||||
<Option value="en">English</Option>
|
{availableLocales.map((locale) => {
|
||||||
<Option value="zh">简体中文</Option>
|
const languageName = new Intl.DisplayNames([locale], { type: "language" }).of(locale);
|
||||||
<Option value="vi">Tiếng Việt</Option>
|
if (languageName === undefined) {
|
||||||
<Option value="fr">French</Option>
|
return (
|
||||||
<Option value="nl">Nederlands</Option>
|
<Option key={locale} value={locale}>
|
||||||
<Option value="sv">Svenska</Option>
|
{locale}
|
||||||
<Option value="de">German</Option>
|
</Option>
|
||||||
<Option value="es">Español</Option>
|
);
|
||||||
<Option value="uk">Українська</Option>
|
}
|
||||||
<Option value="ru">Русский</Option>
|
return (
|
||||||
<Option value="it">Italiano</Option>
|
<Option key={locale} value={locale}>
|
||||||
<Option value="hant">繁體中文</Option>
|
{languageName.charAt(0).toUpperCase() + languageName.slice(1)}
|
||||||
<Option value="tr">Turkish</Option>
|
</Option>
|
||||||
<Option value="ko">한국어</Option>
|
);
|
||||||
<Option value="sl">Slovenščina</Option>
|
})}
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import dayjs from "dayjs";
|
import { getRelativeTimeString } from "@/helpers/datetime";
|
||||||
import { memo, useEffect, useRef, useState } from "react";
|
import { memo, useEffect, useRef, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -19,14 +19,6 @@ interface Props {
|
|||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => {
|
|
||||||
if (Date.now() - time < 1000 * 60 * 60 * 24) {
|
|
||||||
return dayjs(time).locale(locale).fromNow();
|
|
||||||
} else {
|
|
||||||
return dayjs(time).locale(locale).format("YYYY/MM/DD HH:mm:ss");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Memo: React.FC<Props> = (props: Props) => {
|
const Memo: React.FC<Props> = (props: Props) => {
|
||||||
const { memo, readonly } = props;
|
const { memo, readonly } = props;
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
@ -35,7 +27,7 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
const filterStore = useFilterStore();
|
const filterStore = useFilterStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const memoStore = useMemoStore();
|
const memoStore = useMemoStore();
|
||||||
const [createdTimeStr, setCreatedTimeStr] = useState<string>(getFormatedMemoTimeStr(memo.createdTs, i18n.language));
|
const [createdTimeStr, setCreatedTimeStr] = useState<string>(getRelativeTimeString(memo.createdTs));
|
||||||
const memoContainerRef = useRef<HTMLDivElement>(null);
|
const memoContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const isVisitorMode = userStore.isVisitorMode() || readonly;
|
const isVisitorMode = userStore.isVisitorMode() || readonly;
|
||||||
|
|
||||||
@ -43,7 +35,7 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
let intervalFlag: any = -1;
|
let intervalFlag: any = -1;
|
||||||
if (Date.now() - memo.createdTs < 1000 * 60 * 60 * 24) {
|
if (Date.now() - memo.createdTs < 1000 * 60 * 60 * 24) {
|
||||||
intervalFlag = setInterval(() => {
|
intervalFlag = setInterval(() => {
|
||||||
setCreatedTimeStr(getFormatedMemoTimeStr(memo.createdTs, i18n.language));
|
setCreatedTimeStr(getRelativeTimeString(memo.createdTs));
|
||||||
}, 1000 * 1);
|
}, 1000 * 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,8 +82,8 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
const handleDeleteMemoClick = async () => {
|
const handleDeleteMemoClick = async () => {
|
||||||
showCommonDialog({
|
showCommonDialog({
|
||||||
title: "Delete memo",
|
title: t("memo.delete-memo"),
|
||||||
content: "Are you sure to delete this memo?",
|
content: t("memo.delete-confirm"),
|
||||||
style: "warning",
|
style: "warning",
|
||||||
dialogName: "delete-memo-dialog",
|
dialogName: "delete-memo-dialog",
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
|
@ -2,7 +2,7 @@ import { useEffect } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { useFilterStore, useShortcutStore } from "@/store/module";
|
import { useFilterStore, useShortcutStore } from "@/store/module";
|
||||||
import * as utils from "@/helpers/utils";
|
import { getDateString } from "@/helpers/datetime";
|
||||||
import { getTextWithMemoType } from "@/helpers/filter";
|
import { getTextWithMemoType } from "@/helpers/filter";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import "@/less/memo-filter.less";
|
import "@/less/memo-filter.less";
|
||||||
@ -63,7 +63,12 @@ const MemoFilter = () => {
|
|||||||
filterStore.setFromAndToFilter();
|
filterStore.setFromAndToFilter();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon.Calendar className="icon-text" /> {utils.getDateString(duration.from)} to {utils.getDateString(duration.to)}
|
<Icon.Calendar className="icon-text" />
|
||||||
|
{t("common.filter-period", {
|
||||||
|
from: getDateString(duration.from),
|
||||||
|
to: getDateString(duration.to),
|
||||||
|
interpolation: { escapeValue: false },
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div
|
<div
|
||||||
|
@ -3,7 +3,7 @@ import { toast } from "react-hot-toast";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFilterStore, useMemoStore, useShortcutStore, useUserStore } from "@/store/module";
|
import { useFilterStore, useMemoStore, useShortcutStore, useUserStore } from "@/store/module";
|
||||||
import { TAG_REG, LINK_REG } from "@/labs/marked/parser";
|
import { TAG_REG, LINK_REG } from "@/labs/marked/parser";
|
||||||
import * as utils from "@/helpers/utils";
|
import { getTimeStampByDate } from "@/helpers/datetime";
|
||||||
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
||||||
import { checkShouldShowMemoWithFilters } from "@/helpers/filter";
|
import { checkShouldShowMemoWithFilters } from "@/helpers/filter";
|
||||||
import Memo from "./Memo";
|
import Memo from "./Memo";
|
||||||
@ -54,7 +54,7 @@ const MemoList = () => {
|
|||||||
if (
|
if (
|
||||||
duration &&
|
duration &&
|
||||||
duration.from < duration.to &&
|
duration.from < duration.to &&
|
||||||
(utils.getTimeStampByDate(memo.createdTs) < duration.from || utils.getTimeStampByDate(memo.createdTs) > duration.to)
|
(getTimeStampByDate(memo.createdTs) < duration.from || getTimeStampByDate(memo.createdTs) > duration.to)
|
||||||
) {
|
) {
|
||||||
shouldShow = false;
|
shouldShow = false;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import * as utils from "@/helpers/utils";
|
import { getDateTimeString } from "@/helpers/datetime";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import { generateDialog } from "./Dialog";
|
import { generateDialog } from "./Dialog";
|
||||||
import "@/less/preview-image-dialog.less";
|
import "@/less/preview-image-dialog.less";
|
||||||
@ -38,7 +38,7 @@ const PreviewImageDialog: React.FC<Props> = ({ destroy, imgUrls, initialIndex }:
|
|||||||
const handleDownloadBtnClick = () => {
|
const handleDownloadBtnClick = () => {
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = imgUrls[currentIndex];
|
a.href = imgUrls[currentIndex];
|
||||||
a.download = `memos-${utils.getDateTimeString(Date.now())}.png`;
|
a.download = `memos-${getDateTimeString(Date.now())}.png`;
|
||||||
a.click();
|
a.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import dayjs from "dayjs";
|
import { getDateTimeString } from "@/helpers/datetime";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import ResourceCover from "./ResourceCover";
|
import ResourceCover from "./ResourceCover";
|
||||||
@ -32,7 +32,7 @@ const ResourceCard = ({ resource, handleCheckClick, handleUncheckClick }: Resour
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full flex flex-col justify-start items-center px-1 select-none">
|
<div className="w-full flex flex-col justify-start items-center px-1 select-none">
|
||||||
<div className="w-full text-base text-center text-ellipsis overflow-hidden line-clamp-3">{resource.filename}</div>
|
<div className="w-full text-base text-center text-ellipsis overflow-hidden line-clamp-3">{resource.filename}</div>
|
||||||
<div className="text-xs text-gray-400 text-center">{dayjs(resource.createdTs).locale("en").format("YYYY/MM/DD HH:mm:ss")}</div>
|
<div className="text-xs text-gray-400 text-center">{getDateTimeString(resource.createdTs)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -39,8 +39,8 @@ const ResourceItemDropdown = ({ resource }: Props) => {
|
|||||||
|
|
||||||
const handleResetResourceLinkBtnClick = (resource: Resource) => {
|
const handleResetResourceLinkBtnClick = (resource: Resource) => {
|
||||||
showCommonDialog({
|
showCommonDialog({
|
||||||
title: "Reset resource link",
|
title: t("resource.reset-resource-link"),
|
||||||
content: "Are you sure to reset the resource link?",
|
content: t("resource.reset-link-prompt"),
|
||||||
style: "warning",
|
style: "warning",
|
||||||
dialogName: "reset-resource-link-dialog",
|
dialogName: "reset-resource-link-dialog",
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
@ -75,7 +75,7 @@ const ResourceItemDropdown = ({ resource }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
actionsClassName="!w-28"
|
actionsClassName="!w-auto min-w-[8rem]"
|
||||||
trigger={<Icon.MoreVertical className="w-4 h-auto hover:opacity-80 cursor-pointer" />}
|
trigger={<Icon.MoreVertical className="w-4 h-auto hover:opacity-80 cursor-pointer" />}
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
@ -95,7 +95,7 @@ const ResourceItemDropdown = ({ resource }: Props) => {
|
|||||||
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
onClick={() => handleResetResourceLinkBtnClick(resource)}
|
onClick={() => handleResetResourceLinkBtnClick(resource)}
|
||||||
>
|
>
|
||||||
Reset link
|
{t("resource.reset-link")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
|
@ -76,8 +76,8 @@ const PreferencesSection = () => {
|
|||||||
|
|
||||||
const handleArchiveUserClick = (user: User) => {
|
const handleArchiveUserClick = (user: User) => {
|
||||||
showCommonDialog({
|
showCommonDialog({
|
||||||
title: `Archive Member`,
|
title: t("setting.member-section.archive-member"),
|
||||||
content: `❗️Are you sure to archive ${user.username}?`,
|
content: t("setting.member-section.archive-warning", { username: user.username }),
|
||||||
style: "warning",
|
style: "warning",
|
||||||
dialogName: "archive-user-dialog",
|
dialogName: "archive-user-dialog",
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
@ -100,8 +100,8 @@ const PreferencesSection = () => {
|
|||||||
|
|
||||||
const handleDeleteUserClick = (user: User) => {
|
const handleDeleteUserClick = (user: User) => {
|
||||||
showCommonDialog({
|
showCommonDialog({
|
||||||
title: `Delete Member`,
|
title: t("setting.member-section.delete-member"),
|
||||||
content: `Are you sure to delete ${user.username}? THIS ACTION IS IRREVERSIBLE.❗️`,
|
content: t("setting.member-section.delete-warning", { username: user.username }),
|
||||||
style: "warning",
|
style: "warning",
|
||||||
dialogName: "delete-user-dialog",
|
dialogName: "delete-user-dialog",
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
|
@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useUserStore } from "@/store/module";
|
import { useUserStore } from "@/store/module";
|
||||||
import { showCommonDialog } from "../Dialog/CommonDialog";
|
import { showCommonDialog } from "../Dialog/CommonDialog";
|
||||||
import showChangePasswordDialog from "../ChangePasswordDialog";
|
import showChangePasswordDialog from "../ChangePasswordDialog";
|
||||||
|
import Icon from "../Icon";
|
||||||
import showUpdateAccountDialog from "../UpdateAccountDialog";
|
import showUpdateAccountDialog from "../UpdateAccountDialog";
|
||||||
import UserAvatar from "../UserAvatar";
|
import UserAvatar from "../UserAvatar";
|
||||||
import "@/less/settings/my-account-section.less";
|
import "@/less/settings/my-account-section.less";
|
||||||
@ -14,8 +15,8 @@ const MyAccountSection = () => {
|
|||||||
|
|
||||||
const handleResetOpenIdBtnClick = async () => {
|
const handleResetOpenIdBtnClick = async () => {
|
||||||
showCommonDialog({
|
showCommonDialog({
|
||||||
title: "Reset Open API",
|
title: t("setting.account-section.openapi-reset"),
|
||||||
content: "❗️The existing API will be invalidated and a new one will be generated, are you sure you want to reset?",
|
content: t("setting.account-section.openapi-reset-warning"),
|
||||||
style: "warning",
|
style: "warning",
|
||||||
dialogName: "reset-openid-dialog",
|
dialogName: "reset-openid-dialog",
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
@ -47,13 +48,16 @@ const MyAccountSection = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="section-container openapi-section-container">
|
<div className="section-container openapi-section-container">
|
||||||
<p className="title-text">Open API</p>
|
<p className="title-text">{t("setting.account-section.openapi-title")}</p>
|
||||||
<p className="value-text">{openAPIRoute}</p>
|
<p className="value-text">{openAPIRoute}</p>
|
||||||
<span className="btn-danger mt-2" onClick={handleResetOpenIdBtnClick}>
|
<span className="btn-danger mt-2" onClick={handleResetOpenIdBtnClick}>
|
||||||
{t("common.reset")} API
|
{t("setting.account-section.reset-api")} <Icon.RefreshCw className="ml-2 h-4 w-4" />
|
||||||
</span>
|
</span>
|
||||||
<div className="usage-guide-container">
|
<div className="usage-guide-container">
|
||||||
<pre>{`POST ${openAPIRoute}\nContent-type: application/json\n{\n "content": "Hello #memos from ${window.location.origin}"\n}`}</pre>
|
<pre>{`POST ${openAPIRoute}\nContent-type: application/json\n{\n "content": "${t("setting.account-section.openapi-sample-post", {
|
||||||
|
url: window.location.origin,
|
||||||
|
interpolation: { escapeValue: false },
|
||||||
|
})}"\n}`}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -23,8 +23,8 @@ const SSOSection = () => {
|
|||||||
|
|
||||||
const handleDeleteIdentityProvider = async (identityProvider: IdentityProvider) => {
|
const handleDeleteIdentityProvider = async (identityProvider: IdentityProvider) => {
|
||||||
showCommonDialog({
|
showCommonDialog({
|
||||||
title: "Confirm delete",
|
title: t("setting.sso-section.delete-sso"),
|
||||||
content: "Are you sure to delete this SSO? THIS ACTION IS IRREVERSIBLE❗",
|
content: t("setting.sso-section.confirm-delete", { name: identityProvider.name }),
|
||||||
style: "warning",
|
style: "warning",
|
||||||
dialogName: "delete-identity-provider-dialog",
|
dialogName: "delete-identity-provider-dialog",
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
@ -42,7 +42,7 @@ const SSOSection = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="section-container">
|
<div className="section-container">
|
||||||
<div className="mt-4 mb-2 w-full flex flex-row justify-start items-center">
|
<div className="mt-4 mb-2 w-full flex flex-row justify-start items-center">
|
||||||
<span className="font-mono text-sm text-gray-400 mr-2">SSO List</span>
|
<span className="font-mono text-sm text-gray-400 mr-2">{t("setting.sso-section.sso-list")}</span>
|
||||||
<button
|
<button
|
||||||
className="btn-normal px-2 py-0 leading-7"
|
className="btn-normal px-2 py-0 leading-7"
|
||||||
onClick={() => showCreateIdentityProviderDialog(undefined, fetchIdentityProviderList)}
|
onClick={() => showCreateIdentityProviderDialog(undefined, fetchIdentityProviderList)}
|
||||||
@ -68,7 +68,7 @@ const SSOSection = () => {
|
|||||||
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
onClick={() => showCreateIdentityProviderDialog(identityProvider, fetchIdentityProviderList)}
|
onClick={() => showCreateIdentityProviderDialog(identityProvider, fetchIdentityProviderList)}
|
||||||
>
|
>
|
||||||
Edit
|
{t("common.edit")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100 dark:hover:bg-zinc-600"
|
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
|
@ -39,7 +39,7 @@ const StorageSection = () => {
|
|||||||
const handleDeleteStorage = (storage: ObjectStorage) => {
|
const handleDeleteStorage = (storage: ObjectStorage) => {
|
||||||
showCommonDialog({
|
showCommonDialog({
|
||||||
title: t("setting.storage-section.delete-storage"),
|
title: t("setting.storage-section.delete-storage"),
|
||||||
content: t("setting.storage-section.warning-text"),
|
content: t("setting.storage-section.warning-text", { name: storage.name }),
|
||||||
style: "warning",
|
style: "warning",
|
||||||
dialogName: "delete-storage-dialog",
|
dialogName: "delete-storage-dialog",
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
@ -57,7 +57,7 @@ const StorageSection = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="section-container">
|
<div className="section-container">
|
||||||
<div className="mt-4 mb-2 w-full flex flex-row justify-start items-center">
|
<div className="mt-4 mb-2 w-full flex flex-row justify-start items-center">
|
||||||
<span className="font-mono text-sm text-gray-400 mr-2">Current storage</span>
|
<span className="font-mono text-sm text-gray-400 mr-2">{t("setting.storage-section.current-storage")}</span>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
className="w-full mb-4"
|
className="w-full mb-4"
|
||||||
@ -66,8 +66,8 @@ const StorageSection = () => {
|
|||||||
handleActiveStorageServiceChanged(storageId ?? storageServiceId);
|
handleActiveStorageServiceChanged(storageId ?? storageServiceId);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Option value={0}>Database</Option>
|
<Option value={0}>{t("setting.storage-section.type-database")}</Option>
|
||||||
<Option value={-1}>Local</Option>
|
<Option value={-1}>{t("setting.storage-section.type-local")}</Option>
|
||||||
{storageList.map((storage) => (
|
{storageList.map((storage) => (
|
||||||
<Option key={storage.id} value={storage.id}>
|
<Option key={storage.id} value={storage.id}>
|
||||||
{storage.name}
|
{storage.name}
|
||||||
@ -84,7 +84,7 @@ const StorageSection = () => {
|
|||||||
<div className="mt-2 w-full flex flex-col">
|
<div className="mt-2 w-full flex flex-col">
|
||||||
<div className="py-2 w-full border-t dark:border-zinc-700 flex flex-row items-center justify-between">
|
<div className="py-2 w-full border-t dark:border-zinc-700 flex flex-row items-center justify-between">
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<p className="ml-2">Local</p>
|
<p className="ml-2">{t("setting.storage-section.type-local")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@ -95,7 +95,7 @@ const StorageSection = () => {
|
|||||||
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
onClick={() => showUpdateLocalStorageDialog(systemStatus.localStoragePath)}
|
onClick={() => showUpdateLocalStorageDialog(systemStatus.localStoragePath)}
|
||||||
>
|
>
|
||||||
Edit
|
{t("common.edit")}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@ -119,7 +119,7 @@ const StorageSection = () => {
|
|||||||
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
onClick={() => showCreateStorageServiceDialog(storage, fetchStorageList)}
|
onClick={() => showCreateStorageServiceDialog(storage, fetchStorageList)}
|
||||||
>
|
>
|
||||||
Edit
|
{t("common.edit")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100 dark:hover:bg-zinc-600"
|
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, Divider, Input, Switch, Textarea } from "@mui/joy";
|
import { Button, Divider, Input, Switch, Textarea, Typography } from "@mui/joy";
|
||||||
|
import { formatBytes } from "@/helpers/utils";
|
||||||
import { useGlobalStore } from "@/store/module";
|
import { useGlobalStore } from "@/store/module";
|
||||||
import * as api from "@/helpers/api";
|
import * as api from "@/helpers/api";
|
||||||
|
import Icon from "../Icon";
|
||||||
import showUpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog";
|
import showUpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog";
|
||||||
import "@/less/settings/system-section.less";
|
import "@/less/settings/system-section.less";
|
||||||
|
|
||||||
@ -16,15 +18,6 @@ interface State {
|
|||||||
additionalScript: string;
|
additionalScript: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatBytes = (bytes: number) => {
|
|
||||||
if (bytes <= 0) return "0 Bytes";
|
|
||||||
const k = 1024,
|
|
||||||
dm = 2,
|
|
||||||
sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
|
|
||||||
i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + sizes[i];
|
|
||||||
};
|
|
||||||
|
|
||||||
const SystemSection = () => {
|
const SystemSection = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const globalStore = useGlobalStore();
|
const globalStore = useGlobalStore();
|
||||||
@ -203,7 +196,7 @@ const SystemSection = () => {
|
|||||||
<Switch checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} />
|
<Switch checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="form-label">
|
<div className="form-label">
|
||||||
<span className="normal-text">Ignore version upgrade</span>
|
<span className="normal-text">{t("setting.system-section.ignore-version-upgrade")}</span>
|
||||||
<Switch checked={state.ignoreUpgrade} onChange={(event) => handleIgnoreUpgradeChanged(event.target.checked)} />
|
<Switch checked={state.ignoreUpgrade} onChange={(event) => handleIgnoreUpgradeChanged(event.target.checked)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="form-label">
|
<div className="form-label">
|
||||||
@ -212,7 +205,17 @@ const SystemSection = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Divider className="!mt-3 !my-4" />
|
<Divider className="!mt-3 !my-4" />
|
||||||
<div className="form-label">
|
<div className="form-label">
|
||||||
<span className="normal-text">OpenAI API Key</span>
|
<span className="normal-text">{t("setting.system-section.openai-api-key")}</span>
|
||||||
|
<Typography className="!mb-1" level="body2">
|
||||||
|
<a
|
||||||
|
className="ml-2 text-sm text-blue-600 hover:opacity-80 hover:underline"
|
||||||
|
href="https://platform.openai.com/account/api-keys"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{t("setting.system-section.openai-api-key-description")}
|
||||||
|
<Icon.ExternalLink className="inline -mt-1 ml-1 w-4 h-auto opacity-80" />
|
||||||
|
</a>
|
||||||
|
</Typography>
|
||||||
<Button onClick={handleSaveOpenAIConfig}>{t("common.save")}</Button>
|
<Button onClick={handleSaveOpenAIConfig}>{t("common.save")}</Button>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
@ -221,12 +224,12 @@ const SystemSection = () => {
|
|||||||
fontFamily: "monospace",
|
fontFamily: "monospace",
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
}}
|
}}
|
||||||
placeholder="OpenAI API Key"
|
placeholder={t("setting.system-section.openai-api-key-placeholder")}
|
||||||
value={openAIConfig.key}
|
value={openAIConfig.key}
|
||||||
onChange={(event) => handleOpenAIConfigKeyChanged(event.target.value)}
|
onChange={(event) => handleOpenAIConfigKeyChanged(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className="form-label mt-2">
|
<div className="form-label mt-2">
|
||||||
<span className="normal-text">OpenAI API Host</span>
|
<span className="normal-text">{t("setting.system-section.openai-api-host")}</span>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@ -234,7 +237,7 @@ const SystemSection = () => {
|
|||||||
fontFamily: "monospace",
|
fontFamily: "monospace",
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
}}
|
}}
|
||||||
placeholder="OpenAI API Host. Default: https://api.openai.com"
|
placeholder={t("setting.system-section.openai-api-host-placeholder")}
|
||||||
value={openAIConfig.host}
|
value={openAIConfig.host}
|
||||||
onChange={(event) => handleOpenAIConfigHostChanged(event.target.value)}
|
onChange={(event) => handleOpenAIConfigHostChanged(event.target.value)}
|
||||||
/>
|
/>
|
||||||
|
@ -8,7 +8,7 @@ import { toLower } from "lodash-es";
|
|||||||
import toImage from "@/labs/html2image";
|
import toImage from "@/labs/html2image";
|
||||||
import { useGlobalStore, useMemoStore, useUserStore } from "@/store/module";
|
import { useGlobalStore, useMemoStore, useUserStore } from "@/store/module";
|
||||||
import { VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts";
|
import { VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts";
|
||||||
import * as utils from "@/helpers/utils";
|
import { getDateTimeString, getTimeStampByDate } from "@/helpers/datetime";
|
||||||
import { getMemoStats } from "@/helpers/api";
|
import { getMemoStats } from "@/helpers/api";
|
||||||
import useLoading from "@/hooks/useLoading";
|
import useLoading from "@/hooks/useLoading";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
@ -46,9 +46,9 @@ const ShareMemoDialog: React.FC<Props> = (props: Props) => {
|
|||||||
const memoElRef = useRef<HTMLDivElement>(null);
|
const memoElRef = useRef<HTMLDivElement>(null);
|
||||||
const memo = {
|
const memo = {
|
||||||
...propsMemo,
|
...propsMemo,
|
||||||
createdAtStr: utils.getDateTimeString(propsMemo.createdTs),
|
createdAtStr: getDateTimeString(propsMemo.createdTs),
|
||||||
};
|
};
|
||||||
const createdDays = Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24);
|
const createdDays = Math.ceil((Date.now() - getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getMemoStats(user.id)
|
getMemoStats(user.id)
|
||||||
@ -87,7 +87,7 @@ const ShareMemoDialog: React.FC<Props> = (props: Props) => {
|
|||||||
.then((url) => {
|
.then((url) => {
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = `memos-${utils.getDateTimeString(Date.now())}.png`;
|
a.download = `memos-${getDateTimeString(Date.now())}.png`;
|
||||||
a.click();
|
a.click();
|
||||||
|
|
||||||
createLoadingState.setFinish();
|
createLoadingState.setFinish();
|
||||||
|
@ -2,7 +2,7 @@ import { useEffect } from "react";
|
|||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFilterStore, useShortcutStore } from "@/store/module";
|
import { useFilterStore, useShortcutStore } from "@/store/module";
|
||||||
import * as utils from "@/helpers/utils";
|
import { getTimeStampByDate } from "@/helpers/datetime";
|
||||||
import useToggle from "@/hooks/useToggle";
|
import useToggle from "@/hooks/useToggle";
|
||||||
import useLoading from "@/hooks/useLoading";
|
import useLoading from "@/hooks/useLoading";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
@ -18,10 +18,10 @@ const ShortcutList = () => {
|
|||||||
|
|
||||||
const pinnedShortcuts = shortcuts
|
const pinnedShortcuts = shortcuts
|
||||||
.filter((s) => s.rowStatus === "ARCHIVED")
|
.filter((s) => s.rowStatus === "ARCHIVED")
|
||||||
.sort((a, b) => utils.getTimeStampByDate(b.createdTs) - utils.getTimeStampByDate(a.createdTs));
|
.sort((a, b) => getTimeStampByDate(b.createdTs) - getTimeStampByDate(a.createdTs));
|
||||||
const unpinnedShortcuts = shortcuts
|
const unpinnedShortcuts = shortcuts
|
||||||
.filter((s) => s.rowStatus === "NORMAL")
|
.filter((s) => s.rowStatus === "NORMAL")
|
||||||
.sort((a, b) => utils.getTimeStampByDate(b.createdTs) - utils.getTimeStampByDate(a.createdTs));
|
.sort((a, b) => getTimeStampByDate(b.createdTs) - getTimeStampByDate(a.createdTs));
|
||||||
const sortedShortcuts = pinnedShortcuts.concat(unpinnedShortcuts);
|
const sortedShortcuts = pinnedShortcuts.concat(unpinnedShortcuts);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -146,17 +146,17 @@ const UpdateAccountDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{t("common.username")}
|
{t("common.username")}
|
||||||
<span className="text-sm text-gray-400 ml-1">(Using to sign in)</span>
|
<span className="text-sm text-gray-400 ml-1">{t("setting.account-section.username-note")}</span>
|
||||||
</p>
|
</p>
|
||||||
<input type="text" className="input-text" value={state.username} onChange={handleUsernameChanged} />
|
<input type="text" className="input-text" value={state.username} onChange={handleUsernameChanged} />
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{t("common.nickname")}
|
{t("common.nickname")}
|
||||||
<span className="text-sm text-gray-400 ml-1">(Display in the banner)</span>
|
<span className="text-sm text-gray-400 ml-1">{t("setting.account-section.nickname-note")}</span>
|
||||||
</p>
|
</p>
|
||||||
<input type="text" className="input-text" value={state.nickname} onChange={handleNicknameChanged} />
|
<input type="text" className="input-text" value={state.nickname} onChange={handleNicknameChanged} />
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{t("common.email")}
|
{t("common.email")}
|
||||||
<span className="text-sm text-gray-400 ml-1">(Optional)</span>
|
<span className="text-sm text-gray-400 ml-1">{t("setting.account-section.email-note")}</span>
|
||||||
</p>
|
</p>
|
||||||
<input type="text" className="input-text" value={state.email} onChange={handleEmailChanged} />
|
<input type="text" className="input-text" value={state.email} onChange={handleEmailChanged} />
|
||||||
<div className="pt-2 w-full flex flex-row justify-end items-center space-x-2">
|
<div className="pt-2 w-full flex flex-row justify-end items-center space-x-2">
|
||||||
|
@ -71,7 +71,7 @@ const UpdateCustomizedProfileDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
|
|
||||||
const handleSaveButtonClick = async () => {
|
const handleSaveButtonClick = async () => {
|
||||||
if (state.name === "") {
|
if (state.name === "") {
|
||||||
toast.error("Please fill server name");
|
toast.error(t("message.fill-server-name"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,11 +105,11 @@ const UpdateCustomizedProfileDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
<input type="text" className="input-text" value={state.name} onChange={handleNameChanged} />
|
<input type="text" className="input-text" value={state.name} onChange={handleNameChanged} />
|
||||||
<p className="text-sm mb-1 mt-2">{t("setting.system-section.customize-server.icon-url")}</p>
|
<p className="text-sm mb-1 mt-2">{t("setting.system-section.customize-server.icon-url")}</p>
|
||||||
<input type="text" className="input-text" value={state.logoUrl} onChange={handleLogoUrlChanged} />
|
<input type="text" className="input-text" value={state.logoUrl} onChange={handleLogoUrlChanged} />
|
||||||
<p className="text-sm mb-1 mt-2">Description</p>
|
<p className="text-sm mb-1 mt-2">{t("setting.system-section.customize-server.description")}</p>
|
||||||
<input type="text" className="input-text" value={state.description} onChange={handleDescriptionChanged} />
|
<input type="text" className="input-text" value={state.description} onChange={handleDescriptionChanged} />
|
||||||
<p className="text-sm mb-1 mt-2">Server locale</p>
|
<p className="text-sm mb-1 mt-2">{t("setting.system-section.customize-server.locale")}</p>
|
||||||
<LocaleSelect className="!w-full" value={state.locale} onChange={handleLocaleSelectChange} />
|
<LocaleSelect className="!w-full" value={state.locale} onChange={handleLocaleSelectChange} />
|
||||||
<p className="text-sm mb-1 mt-2">Server appearance</p>
|
<p className="text-sm mb-1 mt-2">{t("setting.system-section.customize-server.appearance")}</p>
|
||||||
<AppearanceSelect className="!w-full" value={state.appearance} onChange={handleAppearanceSelectChange} />
|
<AppearanceSelect className="!w-full" value={state.appearance} onChange={handleAppearanceSelectChange} />
|
||||||
<div className="mt-4 w-full flex flex-row justify-between items-center space-x-2">
|
<div className="mt-4 w-full flex flex-row justify-between items-center space-x-2">
|
||||||
<div className="flex flex-row justify-start items-center">
|
<div className="flex flex-row justify-start items-center">
|
||||||
|
@ -6,6 +6,7 @@ import * as api from "@/helpers/api";
|
|||||||
import { generateDialog } from "./Dialog";
|
import { generateDialog } from "./Dialog";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import LearnMore from "./LearnMore";
|
import LearnMore from "./LearnMore";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props extends DialogProps {
|
interface Props extends DialogProps {
|
||||||
localStoragePath?: string;
|
localStoragePath?: string;
|
||||||
@ -13,6 +14,7 @@ interface Props extends DialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const UpdateLocalStorageDialog: React.FC<Props> = (props: Props) => {
|
const UpdateLocalStorageDialog: React.FC<Props> = (props: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { destroy, localStoragePath, confirmCallback } = props;
|
const { destroy, localStoragePath, confirmCallback } = props;
|
||||||
const globalStore = useGlobalStore();
|
const globalStore = useGlobalStore();
|
||||||
const [path, setPath] = useState(localStoragePath || "");
|
const [path, setPath] = useState(localStoragePath || "");
|
||||||
@ -41,23 +43,31 @@ const UpdateLocalStorageDialog: React.FC<Props> = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dialog-header-container">
|
<div className="dialog-header-container">
|
||||||
<p className="title-text">Update local storage path</p>
|
<p className="title-text">{t("setting.storage-section.update-local-path")}</p>
|
||||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||||
<Icon.X />
|
<Icon.X />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container max-w-xs">
|
<div className="dialog-content-container max-w-xs">
|
||||||
<p className="text-sm break-words mb-1">
|
<p className="text-sm break-words mb-1">
|
||||||
{"Local storage path is a relative path to your database file."}
|
{t("setting.storage-section.update-local-path-description")}
|
||||||
<LearnMore className="ml-1" url="https://usememos.com/docs/local-storage" />
|
<LearnMore className="ml-1" url="https://usememos.com/docs/local-storage" />
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-400 mb-2 break-all">{"e.g. assets/{timestamp}_{filename}"}</p>
|
<p className="text-sm text-gray-400 mb-2 break-all">
|
||||||
<Input className="mb-2" placeholder="Local storage path" fullWidth value={path} onChange={(e) => setPath(e.target.value)} />
|
{t("common.e.g")} {"assets/{timestamp}_{filename}"}
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
className="mb-2"
|
||||||
|
placeholder={t("setting.storage-section.local-storage-path")}
|
||||||
|
fullWidth
|
||||||
|
value={path}
|
||||||
|
onChange={(e) => setPath(e.target.value)}
|
||||||
|
/>
|
||||||
<div className="mt-2 w-full flex flex-row justify-end items-center space-x-1">
|
<div className="mt-2 w-full flex flex-row justify-end items-center space-x-1">
|
||||||
<Button variant="plain" color="neutral" onClick={handleCloseBtnClick}>
|
<Button variant="plain" color="neutral" onClick={handleCloseBtnClick}>
|
||||||
Cancel
|
{t("common.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleConfirmBtnClick}>Update</Button>
|
<Button onClick={handleConfirmBtnClick}>{t("common.update")}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -3,6 +3,7 @@ import { useFilterStore, useMemoStore, useUserStore } from "../store/module";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getMemoStats } from "@/helpers/api";
|
import { getMemoStats } from "@/helpers/api";
|
||||||
import { DAILY_TIMESTAMP } from "@/helpers/consts";
|
import { DAILY_TIMESTAMP } from "@/helpers/consts";
|
||||||
|
import { getDateStampByDate, getDateString, getTimeStampByDate } from "@/helpers/datetime";
|
||||||
import * as utils from "@/helpers/utils";
|
import * as utils from "@/helpers/utils";
|
||||||
import "@/less/usage-heat-map.less";
|
import "@/less/usage-heat-map.less";
|
||||||
|
|
||||||
@ -32,7 +33,7 @@ const UsageHeatMap = () => {
|
|||||||
const filterStore = useFilterStore();
|
const filterStore = useFilterStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const memoStore = useMemoStore();
|
const memoStore = useMemoStore();
|
||||||
const todayTimeStamp = utils.getDateStampByDate(Date.now());
|
const todayTimeStamp = getDateStampByDate(Date.now());
|
||||||
const todayDay = new Date(todayTimeStamp).getDay() + 1;
|
const todayDay = new Date(todayTimeStamp).getDay() + 1;
|
||||||
const nullCell = new Array(7 - todayDay).fill(0);
|
const nullCell = new Array(7 - todayDay).fill(0);
|
||||||
const usedDaysAmount = (tableConfig.width - 1) * tableConfig.height + todayDay;
|
const usedDaysAmount = (tableConfig.width - 1) * tableConfig.height + todayDay;
|
||||||
@ -48,7 +49,7 @@ const UsageHeatMap = () => {
|
|||||||
if (!userStore.state.user) {
|
if (!userStore.state.user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCreatedDays(Math.ceil((Date.now() - utils.getTimeStampByDate(userStore.state.user.createdTs)) / 1000 / 3600 / 24));
|
setCreatedDays(Math.ceil((Date.now() - getTimeStampByDate(userStore.state.user.createdTs)) / 1000 / 3600 / 24));
|
||||||
}, [userStore.state.user]);
|
}, [userStore.state.user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -57,7 +58,7 @@ const UsageHeatMap = () => {
|
|||||||
setMemoAmount(data.length);
|
setMemoAmount(data.length);
|
||||||
const newStat: DailyUsageStat[] = getInitialUsageStat(usedDaysAmount, beginDayTimestamp);
|
const newStat: DailyUsageStat[] = getInitialUsageStat(usedDaysAmount, beginDayTimestamp);
|
||||||
for (const record of data) {
|
for (const record of data) {
|
||||||
const index = (utils.getDateStampByDate(record * 1000) - beginDayTimestamp) / (1000 * 3600 * 24) - 1;
|
const index = (getDateStampByDate(record * 1000) - beginDayTimestamp) / (1000 * 3600 * 24) - 1;
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
// because of dailight savings, some days may be 23 hours long instead of 24 hours long
|
// because of dailight savings, some days may be 23 hours long instead of 24 hours long
|
||||||
// this causes the calculations to yield weird indices such as 40.93333333333
|
// this causes the calculations to yield weird indices such as 40.93333333333
|
||||||
@ -83,7 +84,8 @@ const UsageHeatMap = () => {
|
|||||||
const bounding = utils.getElementBounding(event.target as HTMLElement);
|
const bounding = utils.getElementBounding(event.target as HTMLElement);
|
||||||
tempDiv.style.left = bounding.left + "px";
|
tempDiv.style.left = bounding.left + "px";
|
||||||
tempDiv.style.top = bounding.top - 2 + "px";
|
tempDiv.style.top = bounding.top - 2 + "px";
|
||||||
tempDiv.innerHTML = `${item.count} memos on <span className="date-text">${new Date(item.timestamp as number).toDateString()}</span>`;
|
const tMemoOnOpts = { amount: item.count, date: getDateString(item.timestamp as number) };
|
||||||
|
tempDiv.innerHTML = item.count === 1 ? t("heatmap.memo-on", tMemoOnOpts) : t("heatmap.memos-on", tMemoOnOpts);
|
||||||
document.body.appendChild(tempDiv);
|
document.body.appendChild(tempDiv);
|
||||||
|
|
||||||
if (tempDiv.offsetLeft - tempDiv.clientWidth / 2 < 0) {
|
if (tempDiv.offsetLeft - tempDiv.clientWidth / 2 < 0) {
|
||||||
@ -106,6 +108,10 @@ const UsageHeatMap = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// This interpolation is not being used because of the current styling,
|
||||||
|
// but it can improve translation quality by giving it a more meaningful context
|
||||||
|
const tMemoInOpts = { amount: "", period: "", date: "" };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="usage-heat-map-wrapper" ref={containerElRef}>
|
<div className="usage-heat-map-wrapper" ref={containerElRef}>
|
||||||
@ -156,8 +162,10 @@ const UsageHeatMap = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="w-full pl-4 text-xs -mt-2 mb-3 text-gray-400 dark:text-zinc-400">
|
<p className="w-full pl-4 text-xs -mt-2 mb-3 text-gray-400 dark:text-zinc-400">
|
||||||
<span className="font-medium text-gray-500 dark:text-zinc-300">{memoAmount}</span> memos in{" "}
|
<span className="font-medium text-gray-500 dark:text-zinc-300 number">{memoAmount} </span>
|
||||||
<span className="font-medium text-gray-500 dark:text-zinc-300">{createdDays}</span> days
|
{memoAmount === 1 ? t("heatmap.memo-in", tMemoInOpts) : t("heatmap.memos-in", tMemoInOpts)}{" "}
|
||||||
|
<span className="font-medium text-gray-500 dark:text-zinc-300">{createdDays} </span>
|
||||||
|
{createdDays === 1 ? t("heatmap.day", tMemoInOpts) : t("heatmap.days", tMemoInOpts)}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
207
web/src/helpers/datetime.ts
Normal file
207
web/src/helpers/datetime.ts
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
import i18n from "@/i18n";
|
||||||
|
|
||||||
|
export function convertToMillis(localSetting: LocalSetting) {
|
||||||
|
const hoursToMillis = localSetting.dailyReviewTimeOffset * 60 * 60 * 1000;
|
||||||
|
return hoursToMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNowTimeStamp(): number {
|
||||||
|
return Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTimeStampByDate(t: Date | number | string): number {
|
||||||
|
if (typeof t === "string") {
|
||||||
|
t = t.replaceAll("-", "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(t).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDateStampByDate(t?: Date | number | string): number {
|
||||||
|
const tsFromDate = getTimeStampByDate(t ? t : Date.now());
|
||||||
|
const d = new Date(tsFromDate);
|
||||||
|
|
||||||
|
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNormalizedDateString(t?: Date | number | string): string {
|
||||||
|
const tsFromDate = getTimeStampByDate(t ? t : Date.now());
|
||||||
|
const d = new Date(tsFromDate);
|
||||||
|
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = d.getMonth() + 1;
|
||||||
|
const date = d.getDate();
|
||||||
|
|
||||||
|
return `${year}/${month}/${date}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a time string to provided time.
|
||||||
|
*
|
||||||
|
* If no date is provided, the current date is used.
|
||||||
|
*
|
||||||
|
* Output is always ``HH:MM`` (24-hour format)
|
||||||
|
*/
|
||||||
|
export function getTimeString(t?: Date | number | string): string {
|
||||||
|
const tsFromDate = getTimeStampByDate(t ? t : Date.now());
|
||||||
|
const d = new Date(tsFromDate);
|
||||||
|
|
||||||
|
const hours = d.getHours();
|
||||||
|
const mins = d.getMinutes();
|
||||||
|
|
||||||
|
const hoursStr = hours < 10 ? "0" + hours : hours;
|
||||||
|
const minsStr = mins < 10 ? "0" + mins : mins;
|
||||||
|
|
||||||
|
return `${hoursStr}:${minsStr}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a localized date and time string to provided time.
|
||||||
|
*
|
||||||
|
* If no date is provided, the current date is used.
|
||||||
|
*
|
||||||
|
* Sample outputs:
|
||||||
|
* - "en" locale: "1/30/2023, 10:05:00 PM"
|
||||||
|
* - "pt-BR" locale: "30/01/2023 22:05:00"
|
||||||
|
* - "pl" locale: "30.01.2023, 22:05:00"
|
||||||
|
*/
|
||||||
|
export function getDateTimeString(t?: Date | number | string, locale = i18n.language): string {
|
||||||
|
const tsFromDate = getTimeStampByDate(t ? t : Date.now());
|
||||||
|
|
||||||
|
return new Date(tsFromDate).toLocaleDateString(locale, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "numeric",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric",
|
||||||
|
second: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a localized date string to provided time.
|
||||||
|
*
|
||||||
|
* If no date is provided, the current date is used.
|
||||||
|
*
|
||||||
|
* Note: This function does not include time information.
|
||||||
|
*
|
||||||
|
* Sample outputs:
|
||||||
|
* - "en" locale: "1/30/2023"
|
||||||
|
* - "pt-BR" locale: "30/01/2023"
|
||||||
|
* - "pl" locale: "30.01.2023"
|
||||||
|
*/
|
||||||
|
export function getDateString(t?: Date | number | string, locale = i18n.language): string {
|
||||||
|
const tsFromDate = getTimeStampByDate(t ? t : Date.now());
|
||||||
|
|
||||||
|
return new Date(tsFromDate).toLocaleDateString(locale, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "numeric",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a localized relative time string to provided time.
|
||||||
|
*
|
||||||
|
* Possible outputs for "long" format and "en" locale:
|
||||||
|
* - "x seconds ago"
|
||||||
|
* - "x minutes ago"
|
||||||
|
* - "x hours ago"
|
||||||
|
* - "yesterday"
|
||||||
|
* - "x days ago"
|
||||||
|
* - "x weeks ago"
|
||||||
|
* - "x months ago"
|
||||||
|
* - "last year"
|
||||||
|
* - "x years ago"
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const getRelativeTimeString = (time: number, locale = i18n.language, formatStyle: "long" | "short" | "narrow" = "long"): string => {
|
||||||
|
const pastTimeMillis = Date.now() - time;
|
||||||
|
const secMillis = 1000;
|
||||||
|
const minMillis = secMillis * 60;
|
||||||
|
const hourMillis = minMillis * 60;
|
||||||
|
const dayMillis = hourMillis * 24;
|
||||||
|
|
||||||
|
// numeric: "auto" provides "yesterday" for 1 day ago, "always" provides "1 day ago"
|
||||||
|
const formatOpts = { style: formatStyle, numeric: "auto" } as Intl.RelativeTimeFormatOptions;
|
||||||
|
|
||||||
|
const relTime = new Intl.RelativeTimeFormat(locale, formatOpts);
|
||||||
|
|
||||||
|
if (pastTimeMillis < minMillis) {
|
||||||
|
return relTime.format(-Math.round(pastTimeMillis / secMillis), "second");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pastTimeMillis < hourMillis) {
|
||||||
|
return relTime.format(-Math.round(pastTimeMillis / minMillis), "minute");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pastTimeMillis < dayMillis) {
|
||||||
|
return relTime.format(-Math.round(pastTimeMillis / hourMillis), "hour");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pastTimeMillis < dayMillis * 7) {
|
||||||
|
return relTime.format(-Math.round(pastTimeMillis / dayMillis), "day");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pastTimeMillis < dayMillis * 30) {
|
||||||
|
return relTime.format(-Math.round(pastTimeMillis / (dayMillis * 7)), "week");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pastTimeMillis < dayMillis * 365) {
|
||||||
|
return relTime.format(-Math.round(pastTimeMillis / (dayMillis * 30)), "month");
|
||||||
|
}
|
||||||
|
|
||||||
|
return relTime.format(-Math.round(pastTimeMillis / (dayMillis * 365)), "year");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This returns the normalized date string of the provided date.
|
||||||
|
* Format is always `YYYY-MM-DDT00:00`.
|
||||||
|
*
|
||||||
|
* If no date is provided, the current date is used.
|
||||||
|
*/
|
||||||
|
export function getNormalizedTimeString(t?: Date | number | string): string {
|
||||||
|
const date = new Date(t ? t : Date.now());
|
||||||
|
|
||||||
|
const yyyy = date.getFullYear();
|
||||||
|
const M = date.getMonth() + 1;
|
||||||
|
const d = date.getDate();
|
||||||
|
const h = date.getHours();
|
||||||
|
const m = date.getMinutes();
|
||||||
|
|
||||||
|
const MM = M < 10 ? "0" + M : M;
|
||||||
|
const dd = d < 10 ? "0" + d : d;
|
||||||
|
const hh = h < 10 ? "0" + h : h;
|
||||||
|
const mm = m < 10 ? "0" + m : m;
|
||||||
|
|
||||||
|
return `${yyyy}-${MM}-${dd}T${hh}:${mm}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This returns the number of **milliseconds** since the Unix Epoch of the provided date.
|
||||||
|
*
|
||||||
|
* If no date is provided, the current date is used.
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* getUnixTimeMillis("2019-01-25 00:00") // 1548381600000
|
||||||
|
* ```
|
||||||
|
* To get a Unix timestamp (the number of seconds since the epoch), use `getUnixTime()`.
|
||||||
|
*/
|
||||||
|
export function getUnixTimeMillis(t?: Date | number | string): number {
|
||||||
|
const date = new Date(t ? t : Date.now());
|
||||||
|
return date.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This returns the Unix timestamp (the number of **seconds** since the Unix Epoch) of the provided date.
|
||||||
|
*
|
||||||
|
* If no date is provided, the current date is used.
|
||||||
|
* ```
|
||||||
|
* getUnixTime("2019-01-25 00:00") // 1548381600
|
||||||
|
* ```
|
||||||
|
* This value is floored to the nearest second, and does not include a milliseconds component.
|
||||||
|
*/
|
||||||
|
export function getUnixTime(t?: Date | number | string): number {
|
||||||
|
const date = new Date(t ? t : Date.now());
|
||||||
|
return Math.floor(date.getTime() / 1000);
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
import dayjs from "dayjs";
|
import { getUnixTimeMillis } from "./datetime";
|
||||||
import { TAG_REG, LINK_REG } from "@/labs/marked/parser";
|
import { TAG_REG, LINK_REG } from "@/labs/marked/parser";
|
||||||
|
|
||||||
export const relationConsts = [
|
export const relationConsts = [
|
||||||
{ text: "And", value: "AND" },
|
{ text: "filter.and", value: "AND" },
|
||||||
{ text: "Or", value: "OR" },
|
{ text: "filter.or", value: "OR" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const filterConsts = {
|
export const filterConsts = {
|
||||||
@ -203,9 +203,9 @@ export const checkShouldShowMemo = (memo: Memo, filter: Filter) => {
|
|||||||
}
|
}
|
||||||
} else if (type === "DISPLAY_TIME") {
|
} else if (type === "DISPLAY_TIME") {
|
||||||
if (operator === "BEFORE") {
|
if (operator === "BEFORE") {
|
||||||
return memo.createdTs < dayjs(value).valueOf();
|
return memo.createdTs < getUnixTimeMillis(value);
|
||||||
} else {
|
} else {
|
||||||
return memo.createdTs >= dayjs(value).valueOf();
|
return memo.createdTs >= getUnixTimeMillis(value);
|
||||||
}
|
}
|
||||||
} else if (type === "VISIBILITY") {
|
} else if (type === "VISIBILITY") {
|
||||||
let matched = memo.visibility === value;
|
let matched = memo.visibility === value;
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export const slogan = "A lightweight, self-hosted memo hub. Open Source and Free forever.";
|
|
@ -1,16 +1,7 @@
|
|||||||
export function convertToMillis(localSetting: LocalSetting) {
|
|
||||||
const hoursToMillis = localSetting.dailyReviewTimeOffset * 60 * 60 * 1000;
|
|
||||||
return hoursToMillis;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isNullorUndefined = (value: any) => {
|
export const isNullorUndefined = (value: any) => {
|
||||||
return value === null || value === undefined;
|
return value === null || value === undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getNowTimeStamp(): number {
|
|
||||||
return Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getOSVersion(): "Windows" | "MacOS" | "Linux" | "Unknown" {
|
export function getOSVersion(): "Windows" | "MacOS" | "Linux" | "Unknown" {
|
||||||
const appVersion = navigator.userAgent;
|
const appVersion = navigator.userAgent;
|
||||||
let detectedOS: "Windows" | "MacOS" | "Linux" | "Unknown" = "Unknown";
|
let detectedOS: "Windows" | "MacOS" | "Linux" | "Unknown" = "Unknown";
|
||||||
@ -26,63 +17,6 @@ export function getOSVersion(): "Windows" | "MacOS" | "Linux" | "Unknown" {
|
|||||||
return detectedOS;
|
return detectedOS;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTimeStampByDate(t: Date | number | string): number {
|
|
||||||
if (typeof t === "string") {
|
|
||||||
t = t.replaceAll("-", "/");
|
|
||||||
}
|
|
||||||
const d = new Date(t);
|
|
||||||
|
|
||||||
return d.getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDateStampByDate(t: Date | number | string): number {
|
|
||||||
const d = new Date(getTimeStampByDate(t));
|
|
||||||
|
|
||||||
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDateString(t: Date | number | string): string {
|
|
||||||
const d = new Date(getTimeStampByDate(t));
|
|
||||||
|
|
||||||
const year = d.getFullYear();
|
|
||||||
const month = d.getMonth() + 1;
|
|
||||||
const date = d.getDate();
|
|
||||||
|
|
||||||
return `${year}/${month}/${date}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTimeString(t: Date | number | string): string {
|
|
||||||
const d = new Date(getTimeStampByDate(t));
|
|
||||||
|
|
||||||
const hours = d.getHours();
|
|
||||||
const mins = d.getMinutes();
|
|
||||||
|
|
||||||
const hoursStr = hours < 10 ? "0" + hours : hours;
|
|
||||||
const minsStr = mins < 10 ? "0" + mins : mins;
|
|
||||||
|
|
||||||
return `${hoursStr}:${minsStr}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For example: 2021-4-8 17:52:17
|
|
||||||
export function getDateTimeString(t: Date | number | string): string {
|
|
||||||
const d = new Date(getTimeStampByDate(t));
|
|
||||||
|
|
||||||
const year = d.getFullYear();
|
|
||||||
const month = d.getMonth() + 1;
|
|
||||||
const date = d.getDate();
|
|
||||||
const hours = d.getHours();
|
|
||||||
const mins = d.getMinutes();
|
|
||||||
const secs = d.getSeconds();
|
|
||||||
|
|
||||||
const monthStr = month < 10 ? "0" + month : month;
|
|
||||||
const dateStr = date < 10 ? "0" + date : date;
|
|
||||||
const hoursStr = hours < 10 ? "0" + hours : hours;
|
|
||||||
const minsStr = mins < 10 ? "0" + mins : mins;
|
|
||||||
const secsStr = secs < 10 ? "0" + secs : secs;
|
|
||||||
|
|
||||||
return `${year}/${monthStr}/${dateStr} ${hoursStr}:${minsStr}:${secsStr}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getElementBounding = (element: HTMLElement, relativeEl?: HTMLElement) => {
|
export const getElementBounding = (element: HTMLElement, relativeEl?: HTMLElement) => {
|
||||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
|
||||||
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft;
|
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft;
|
||||||
@ -162,3 +96,12 @@ export function convertFileToBase64(file: File): Promise<string> {
|
|||||||
reader.onerror = (error) => reject(error);
|
reader.onerror = (error) => reject(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const formatBytes = (bytes: number) => {
|
||||||
|
if (bytes <= 0) return "0 Bytes";
|
||||||
|
const k = 1024,
|
||||||
|
dm = 2,
|
||||||
|
sizes = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"],
|
||||||
|
i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
||||||
|
};
|
||||||
|
112
web/src/i18n.ts
112
web/src/i18n.ts
@ -1,79 +1,57 @@
|
|||||||
import i18n from "i18next";
|
import i18n, { FallbackLng, FallbackLngObjList } from "i18next";
|
||||||
import { initReactI18next } from "react-i18next";
|
import { initReactI18next } from "react-i18next";
|
||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import enLocale from "./locales/en.json";
|
import toast from "react-hot-toast";
|
||||||
import zhLocale from "./locales/zh.json";
|
|
||||||
import viLocale from "./locales/vi.json";
|
|
||||||
import frLocale from "./locales/fr.json";
|
|
||||||
import nlLocale from "./locales/nl.json";
|
|
||||||
import svLocale from "./locales/sv.json";
|
|
||||||
import deLocale from "./locales/de.json";
|
|
||||||
import esLocale from "./locales/es.json";
|
|
||||||
import ukLocale from "./locales/uk.json";
|
|
||||||
import ruLocale from "./locales/ru.json";
|
|
||||||
import itLocale from "./locales/it.json";
|
|
||||||
import hantLocale from "./locales/zh-Hant.json";
|
|
||||||
import trLocale from "./locales/tr.json";
|
|
||||||
import koLocale from "./locales/ko.json";
|
|
||||||
import slLocale from "./locales/sl.json";
|
|
||||||
|
|
||||||
const DETECTION_OPTIONS = {
|
// eslint-disable-next-line prettier/prettier
|
||||||
order: ["navigator"],
|
export const availableLocales = [
|
||||||
};
|
"de",
|
||||||
|
"en",
|
||||||
|
"es",
|
||||||
|
"fr",
|
||||||
|
"it",
|
||||||
|
"ko",
|
||||||
|
"nl",
|
||||||
|
"pl",
|
||||||
|
"pt-BR",
|
||||||
|
"ru",
|
||||||
|
"sl",
|
||||||
|
"sv",
|
||||||
|
"tr",
|
||||||
|
"uk",
|
||||||
|
"vi",
|
||||||
|
"zh-Hans",
|
||||||
|
"zh-Hant",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const fallbacks = {
|
||||||
|
"zh-HK": ["zh-Hant", "en"],
|
||||||
|
"zh-TW": ["zh-Hant", "en"],
|
||||||
|
zh: ["zh-Hans", "en"],
|
||||||
|
} as FallbackLngObjList;
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
.use(LanguageDetector)
|
.use(LanguageDetector)
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
.init({
|
.init({
|
||||||
detection: DETECTION_OPTIONS,
|
detection: {
|
||||||
resources: {
|
order: ["navigator"],
|
||||||
en: {
|
|
||||||
translation: enLocale,
|
|
||||||
},
|
},
|
||||||
zh: {
|
fallbackLng: {
|
||||||
translation: zhLocale,
|
...fallbacks,
|
||||||
},
|
...{ default: ["en"] },
|
||||||
vi: {
|
} as FallbackLng,
|
||||||
translation: viLocale,
|
|
||||||
},
|
|
||||||
fr: {
|
|
||||||
translation: frLocale,
|
|
||||||
},
|
|
||||||
nl: {
|
|
||||||
translation: nlLocale,
|
|
||||||
},
|
|
||||||
sv: {
|
|
||||||
translation: svLocale,
|
|
||||||
},
|
|
||||||
de: {
|
|
||||||
translation: deLocale,
|
|
||||||
},
|
|
||||||
es: {
|
|
||||||
translation: esLocale,
|
|
||||||
},
|
|
||||||
uk: {
|
|
||||||
translation: ukLocale,
|
|
||||||
},
|
|
||||||
ru: {
|
|
||||||
translation: ruLocale,
|
|
||||||
},
|
|
||||||
it: {
|
|
||||||
translation: itLocale,
|
|
||||||
},
|
|
||||||
hant: {
|
|
||||||
translation: hantLocale,
|
|
||||||
},
|
|
||||||
tr: {
|
|
||||||
translation: trLocale,
|
|
||||||
},
|
|
||||||
ko: {
|
|
||||||
translation: koLocale,
|
|
||||||
},
|
|
||||||
sl: {
|
|
||||||
translation: slLocale,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fallbackLng: "en",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const locale of availableLocales) {
|
||||||
|
import(`./locales/${locale}.json`)
|
||||||
|
.then((translation) => {
|
||||||
|
i18n.addResourceBundle(locale, "translation", translation.default, true, true);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(`Failed to load locale "${locale}".\n${err}`, { duration: 5000 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
export type TLocale = typeof availableLocales[number];
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
@apply px-4;
|
@apply px-4;
|
||||||
|
|
||||||
> .dialog-container {
|
> .dialog-container {
|
||||||
@apply w-128 max-w-full;
|
@apply w-180 max-w-full;
|
||||||
|
|
||||||
> .dialog-content-container {
|
> .dialog-content-container {
|
||||||
@apply flex flex-col justify-start items-start;
|
@apply flex flex-col justify-start items-start;
|
||||||
@ -69,11 +69,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.type-selector {
|
&.type-selector {
|
||||||
@apply w-24;
|
@apply w-1/4;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.operator-selector {
|
&.operator-selector {
|
||||||
@apply w-20;
|
@apply w-1/5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.value-selector {
|
&.value-selector {
|
||||||
@ -92,7 +92,7 @@
|
|||||||
@media only screen and (min-width: 640px) {
|
@media only screen and (min-width: 640px) {
|
||||||
max-width: calc(100% - 152px);
|
max-width: calc(100% - 152px);
|
||||||
}
|
}
|
||||||
@apply h-9 px-2 shrink-0 grow mr-1 text-sm rounded border bg-transparent hover:bg-gray-50 sm:min-w-0 min-w-full;
|
@apply h-9 px-2 shrink-0 grow mr-1 text-sm rounded border bg-transparent sm:min-w-0 min-w-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .remove-btn {
|
> .remove-btn {
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .section-selector-container {
|
> .section-selector-container {
|
||||||
@apply hidden sm:flex flex-col justify-start items-start sm:w-44 h-auto sm:h-full shrink-0 rounded-t-lg sm:rounded-none sm:rounded-l-lg p-4 bg-gray-100 dark:bg-zinc-700;
|
@apply hidden sm:flex flex-col justify-start items-start sm:w-52 h-auto sm:h-full shrink-0 rounded-t-lg sm:rounded-none sm:rounded-l-lg p-4 bg-gray-100 dark:bg-zinc-700;
|
||||||
|
|
||||||
> .section-title {
|
> .section-title {
|
||||||
@apply text-sm mt-2 sm:mt-4 first:mt-4 mb-1 font-mono text-gray-400;
|
@apply text-sm mt-2 sm:mt-4 first:mt-4 mb-1 font-mono text-gray-400;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
"common": {
|
||||||
|
"memos-slogan": "A lightweight, self-hosted memo hub. Open Source and Free forever.",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"resources": "Resources",
|
"resources": "Resources",
|
||||||
@ -14,6 +15,7 @@
|
|||||||
"close": "Close",
|
"close": "Close",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
|
"update": "Update",
|
||||||
"change": "Change",
|
"change": "Change",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
@ -31,12 +33,15 @@
|
|||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"explore": "Explore",
|
"explore": "Explore",
|
||||||
"sign-in": "Sign in",
|
"sign-in": "Sign in",
|
||||||
|
"sign-in-with": "Sign in with {{provider}}",
|
||||||
|
"or": "or",
|
||||||
"sign-up": "Sign up",
|
"sign-up": "Sign up",
|
||||||
"sign-out": "Sign out",
|
"sign-out": "Sign out",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"shortcuts": "Shortcuts",
|
"shortcuts": "Shortcuts",
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
|
"filter-period": "{{from}} to {{to}}",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"yourself": "Yourself",
|
"yourself": "Yourself",
|
||||||
"changed": "changed",
|
"changed": "changed",
|
||||||
@ -52,7 +57,10 @@
|
|||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"visibility": "Visibility"
|
"visibility": "Visibility",
|
||||||
|
"learn-more": "Learn more",
|
||||||
|
"e.g": "e.g.",
|
||||||
|
"beta": "Beta"
|
||||||
},
|
},
|
||||||
"router": {
|
"router": {
|
||||||
"back-to-home": "Back to Home"
|
"back-to-home": "Back to Home"
|
||||||
@ -89,16 +97,21 @@
|
|||||||
"protected": "Visible to members",
|
"protected": "Visible to members",
|
||||||
"public": "Public to everyone",
|
"public": "Public to everyone",
|
||||||
"disabled": "Public memos are disabled"
|
"disabled": "Public memos are disabled"
|
||||||
}
|
},
|
||||||
|
"delete-memo": "Delete Memo",
|
||||||
|
"delete-confirm": "Are you sure to delete this memo?\n\nTHIS ACTION IS IRREVERSIBLE❗"
|
||||||
},
|
},
|
||||||
"resource": {
|
"resource": {
|
||||||
"no-resources": "No resources.",
|
"no-resources": "No resources.",
|
||||||
"fetching-data": "fetching data...",
|
"fetching-data": "fetching data...",
|
||||||
"copy-link": "Copy Link",
|
"copy-link": "Copy Link",
|
||||||
|
"reset-link": "Reset Link",
|
||||||
|
"reset-resource-link": "Reset Resource Link",
|
||||||
|
"reset-link-prompt": "Are you sure to reset the link?\nThis will break all current link usages.\n\nTHIS ACTION IS IRREVERSIBLE❗",
|
||||||
"delete-resource": "Delete Resource",
|
"delete-resource": "Delete Resource",
|
||||||
"warning-text": "Are you sure to delete this resource? THIS ACTION IS IRREVERSIBLE❗",
|
"linked-amount": "Linked amount",
|
||||||
"linked-amount": "Linked memo amount",
|
"warning-text": "Are you sure to delete this resource?\n\nTHIS ACTION IS IRREVERSIBLE❗",
|
||||||
"warning-text-unused": "Are you sure to delete these unused resources? THIS ACTION IS IRREVERSIBLE❗",
|
"warning-text-unused": "Are you sure to delete these unused resources?\n\nTHIS ACTION IS IRREVERSIBLE❗",
|
||||||
"no-unused-resources": "No unused resources",
|
"no-unused-resources": "No unused resources",
|
||||||
"delete-selected-resources": "Delete Selected Resources",
|
"delete-selected-resources": "Delete Selected Resources",
|
||||||
"no-files-selected": "No files selected❗",
|
"no-files-selected": "No files selected❗",
|
||||||
@ -115,6 +128,7 @@
|
|||||||
"external-link": {
|
"external-link": {
|
||||||
"option": "External link",
|
"option": "External link",
|
||||||
"link": "Link",
|
"link": "Link",
|
||||||
|
"link-placeholder": "https://the.link.to/your/resource",
|
||||||
"file-name": "File name",
|
"file-name": "File name",
|
||||||
"file-name-placeholder": "File name",
|
"file-name-placeholder": "File name",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
@ -138,7 +152,8 @@
|
|||||||
"tag-name": "TAG_NAME"
|
"tag-name": "TAG_NAME"
|
||||||
},
|
},
|
||||||
"daily-review": {
|
"daily-review": {
|
||||||
"title": "Daily Review"
|
"title": "Daily Review",
|
||||||
|
"no-memos": "Oops, there is nothing."
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"my-account": "My Account",
|
"my-account": "My Account",
|
||||||
@ -150,8 +165,16 @@
|
|||||||
"sso": "SSO",
|
"sso": "SSO",
|
||||||
"account-section": {
|
"account-section": {
|
||||||
"title": "Account Information",
|
"title": "Account Information",
|
||||||
|
"username-note": "Used to sign in",
|
||||||
|
"nickname-note": "Displayed in the banner",
|
||||||
|
"email-note": "Optional",
|
||||||
"update-information": "Update Information",
|
"update-information": "Update Information",
|
||||||
"change-password": "Change password"
|
"change-password": "Change password",
|
||||||
|
"reset-api": "Reset API",
|
||||||
|
"openapi-title": "OpenAPI",
|
||||||
|
"openapi-reset": "Reset OpenAPI Key",
|
||||||
|
"openapi-reset-warning": "❗ The existing API will be invalidated and a new one will be generated.\n\nAre you sure you want to reset?",
|
||||||
|
"openapi-sample-post": "Hello #memos from {{url}}"
|
||||||
},
|
},
|
||||||
"preference-section": {
|
"preference-section": {
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
@ -167,34 +190,93 @@
|
|||||||
"daily-review-time-offset": "Daily Review Time Offset"
|
"daily-review-time-offset": "Daily Review Time Offset"
|
||||||
},
|
},
|
||||||
"storage-section": {
|
"storage-section": {
|
||||||
|
"current-storage": "Current storage",
|
||||||
|
"type-database": "Database",
|
||||||
|
"type-local": "Local",
|
||||||
"storage-services-list": "Storage service list",
|
"storage-services-list": "Storage service list",
|
||||||
"create-a-service": "Create a service",
|
"create-a-service": "Create a service",
|
||||||
"update-a-service": "Update a service",
|
"update-a-service": "Update a service",
|
||||||
"warning-text": "Are you sure to delete this storage service? THIS ACTION IS IRREVERSIBLE❗",
|
"warning-text": "Are you sure to delete storage service \"{{name}}\"?\n\nTHIS ACTION IS IRREVERSIBLE❗",
|
||||||
"delete-storage": "Delete Storage"
|
"delete-storage": "Delete Storage",
|
||||||
|
"local-storage-path": "Local storage path",
|
||||||
|
"update-local-path": "Update Local Storage Path",
|
||||||
|
"update-local-path-description": "Local storage path is a relative path to your database file",
|
||||||
|
"create-storage": "Create Storage",
|
||||||
|
"update-storage": "Update Storage",
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"s3-compatible-url": "S3 Compatible URL",
|
||||||
|
"region": "Region",
|
||||||
|
"region-placeholder": "Region name",
|
||||||
|
"accesskey": "Access key",
|
||||||
|
"accesskey-placeholder": "Access key / Access ID",
|
||||||
|
"secretkey": "Secret key",
|
||||||
|
"secretkey-placeholder": "Secret key / Access Key",
|
||||||
|
"bucket": "Bucket",
|
||||||
|
"bucket-placeholder": "Bucket name",
|
||||||
|
"path": "Storage Path",
|
||||||
|
"path-description": "You can use the same dynamic variables from local storage, like {filename}",
|
||||||
|
"path-placeholder": "custom/path",
|
||||||
|
"url-prefix": "URL prefix",
|
||||||
|
"url-prefix-placeholder": "Custom URL prefix, optional",
|
||||||
|
"url-suffix": "URL suffix",
|
||||||
|
"url-suffix-placeholder": "Custom URL suffix, optional"
|
||||||
},
|
},
|
||||||
"member-section": {
|
"member-section": {
|
||||||
"create-a-member": "Create a member"
|
"create-a-member": "Create a member",
|
||||||
|
"archive-member": "Archive member",
|
||||||
|
"archive-warning": "❗ Are you sure to archive {{username}}?",
|
||||||
|
"delete-member": "Delete Member",
|
||||||
|
"delete-warning": "❗ Are you sure to delete {{username}}?\n\nTHIS ACTION IS IRREVERSIBLE❗"
|
||||||
},
|
},
|
||||||
"system-section": {
|
"system-section": {
|
||||||
"server-name": "Server Name",
|
"server-name": "Server Name",
|
||||||
"customize-server": {
|
"customize-server": {
|
||||||
"title": "Customize Server",
|
"title": "Customize Server",
|
||||||
"default": "Default is memos",
|
"default": "Default is memos",
|
||||||
"icon-url": "Icon URL"
|
"icon-url": "Icon URL",
|
||||||
|
"description": "Description",
|
||||||
|
"locale": "Server Locale",
|
||||||
|
"appearance": "Server Appearance"
|
||||||
},
|
},
|
||||||
"database-file-size": "Database File Size",
|
"database-file-size": "Database File Size",
|
||||||
"allow-user-signup": "Allow user signup",
|
"allow-user-signup": "Allow user signup",
|
||||||
|
"ignore-version-upgrade": "Ignore version upgrade",
|
||||||
"disable-public-memos": "Disable public memos",
|
"disable-public-memos": "Disable public memos",
|
||||||
"additional-style": "Additional style",
|
"additional-style": "Additional style",
|
||||||
"additional-script": "Additional script",
|
"additional-script": "Additional script",
|
||||||
"additional-style-placeholder": "Additional CSS code",
|
"additional-style-placeholder": "Additional CSS code",
|
||||||
"additional-script-placeholder": "Additional JavaScript code"
|
"additional-script-placeholder": "Additional JavaScript code",
|
||||||
|
"openai-api-key": "OpenAI: API Key",
|
||||||
|
"openai-api-key-description": "Get API key",
|
||||||
|
"openai-api-key-placeholder": "Your OpenAI API Key",
|
||||||
|
"openai-api-host": "OpenAI: API Host",
|
||||||
|
"openai-api-host-placeholder": "Default: https://api.openai.com/"
|
||||||
},
|
},
|
||||||
"appearance-option": {
|
"appearance-option": {
|
||||||
"system": "Follow system",
|
"system": "Follow system",
|
||||||
"light": "Always light",
|
"light": "Always light",
|
||||||
"dark": "Always dark"
|
"dark": "Always dark"
|
||||||
|
},
|
||||||
|
"sso-section": {
|
||||||
|
"sso-list": "SSO List",
|
||||||
|
"delete-sso": "Confirm delete",
|
||||||
|
"confirm-delete": "Are you sure to delete \"{{name}}\" SSO configuration?\n\nTHIS ACTION IS IRREVERSIBLE❗",
|
||||||
|
"create-sso": "Create SSO",
|
||||||
|
"update-sso": "Update SSO",
|
||||||
|
"sso-created": "SSO {{name}} created",
|
||||||
|
"sso-updated": "SSO {{name}} updated",
|
||||||
|
"identifier": "Identifier",
|
||||||
|
"display-name": "Display Name",
|
||||||
|
"template": "Template",
|
||||||
|
"custom": "Custom",
|
||||||
|
"identifier-filter": "Identifier Filter",
|
||||||
|
"redirect-url": "Redirect URL",
|
||||||
|
"client-id": "Client ID",
|
||||||
|
"client-secret": "Client secret",
|
||||||
|
"authorization-endpoint": "Authorization endpoint",
|
||||||
|
"token-endpoint": "Token endpoint",
|
||||||
|
"user-endpoint": "User endpoint",
|
||||||
|
"scopes": "Scopes"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
@ -219,7 +301,9 @@
|
|||||||
"linked": "Has links",
|
"linked": "Has links",
|
||||||
"has-attachment": "Has attachments"
|
"has-attachment": "Has attachments"
|
||||||
},
|
},
|
||||||
"text-placeholder": "Starts with ^ to use regex"
|
"text-placeholder": "Starts with ^ to use regex",
|
||||||
|
"and": "And",
|
||||||
|
"or": "Or"
|
||||||
},
|
},
|
||||||
"message": {
|
"message": {
|
||||||
"no-memos": "no memos 🌃",
|
"no-memos": "no memos 🌃",
|
||||||
@ -230,12 +314,15 @@
|
|||||||
"memo-updated-datetime": "Memo created datetime changed.",
|
"memo-updated-datetime": "Memo created datetime changed.",
|
||||||
"invalid-created-datetime": "Invalid created datetime.",
|
"invalid-created-datetime": "Invalid created datetime.",
|
||||||
"change-memo-created-time": "Change memo created time",
|
"change-memo-created-time": "Change memo created time",
|
||||||
|
"change-memo-created-time-warning-1": "THIS IS NOT A NORMAL BEHAVIOR.",
|
||||||
|
"change-memo-created-time-warning-2": "Please make sure you really need it.",
|
||||||
"memo-not-found": "Memo not found.",
|
"memo-not-found": "Memo not found.",
|
||||||
"fill-all": "Please fill in all fields.",
|
"fill-all": "Please fill in all fields.",
|
||||||
"password-not-match": "Passwords do not match.",
|
"password-not-match": "Passwords do not match.",
|
||||||
"new-password-not-match": "New passwords do not match.",
|
"new-password-not-match": "New passwords do not match.",
|
||||||
"image-load-failed": "Image load failed",
|
"image-load-failed": "Image load failed",
|
||||||
"fill-form": "Please fill out this form",
|
"fill-form": "Please fill out this form",
|
||||||
|
"fill-server-name": "Please fill in the server name",
|
||||||
"login-failed": "Login failed",
|
"login-failed": "Login failed",
|
||||||
"signup-failed": "Signup failed",
|
"signup-failed": "Signup failed",
|
||||||
"user-not-found": "User not found",
|
"user-not-found": "User not found",
|
||||||
@ -281,7 +368,22 @@
|
|||||||
"embed-memo": {
|
"embed-memo": {
|
||||||
"title": "Embed Memo",
|
"title": "Embed Memo",
|
||||||
"text": "Copy and paste the below codes into your blog or website.",
|
"text": "Copy and paste the below codes into your blog or website.",
|
||||||
"only-public-supported": "* Only the public memo supports.",
|
"only-public-supported": "* Only public memos can be embedded.",
|
||||||
"copy": "Copy"
|
"copy": "Copy"
|
||||||
|
},
|
||||||
|
"heatmap": {
|
||||||
|
"memo-in": "{{amount}} memo in {{period}}",
|
||||||
|
"memos-in": "{{amount}} memos in {{period}}",
|
||||||
|
"memo-on": "{{amount}} memo on {{date}}",
|
||||||
|
"memos-on": "{{amount}} memos on {{date}}",
|
||||||
|
"day": "day",
|
||||||
|
"days": "days"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"about-memos": "About Memos",
|
||||||
|
"memos-description": "Memos is a web-based note-taking application that you can use to write, organize, and share notes.",
|
||||||
|
"no-server-description": "No description configured for this server.",
|
||||||
|
"powered-by": "Powered by",
|
||||||
|
"other-projects": "Other Projects"
|
||||||
}
|
}
|
||||||
}
|
}
|
389
web/src/locales/pt-BR.json
Normal file
389
web/src/locales/pt-BR.json
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"memos-slogan": "Uma central de anotações leve e auto-hospedada. De código aberto e gratuito para sempre.",
|
||||||
|
"about": "Sobre",
|
||||||
|
"home": "Início",
|
||||||
|
"resources": "Recursos",
|
||||||
|
"settings": "Configurações",
|
||||||
|
"archived": "Arquivado",
|
||||||
|
"email": "Email",
|
||||||
|
"password": "Senha",
|
||||||
|
"avatar": "Avatar",
|
||||||
|
"username": "Nome de usuário",
|
||||||
|
"nickname": "Apelido",
|
||||||
|
"save": "Salvar",
|
||||||
|
"close": "Fechar",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"create": "Criar",
|
||||||
|
"update": "Atualizar",
|
||||||
|
"change": "Alterar",
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"reset": "Redefinir",
|
||||||
|
"language": "Idioma",
|
||||||
|
"version": "Versão",
|
||||||
|
"pin": "Fixar",
|
||||||
|
"unpin": "Desafixar",
|
||||||
|
"edit": "Editar",
|
||||||
|
"restore": "Restaurar",
|
||||||
|
"delete": "Deletar",
|
||||||
|
"null": "Nulo",
|
||||||
|
"share": "Compartilhar",
|
||||||
|
"archive": "Arquivar",
|
||||||
|
"basic": "Básico",
|
||||||
|
"admin": "Admin",
|
||||||
|
"explore": "Explorar",
|
||||||
|
"sign-in": "Entrar",
|
||||||
|
"sign-in-with": "Entrar usando {{provider}}",
|
||||||
|
"or": "ou",
|
||||||
|
"sign-up": "Registrar",
|
||||||
|
"sign-out": "Sair",
|
||||||
|
"type": "Tipo",
|
||||||
|
"shortcuts": "Atalhos",
|
||||||
|
"title": "Título",
|
||||||
|
"filter": "Filtro",
|
||||||
|
"filter-period": "{{from}} até {{to}}",
|
||||||
|
"tags": "Tags",
|
||||||
|
"yourself": "Você mesmo",
|
||||||
|
"changed": "alterado",
|
||||||
|
"fold": "Recolher",
|
||||||
|
"expand": "Expandir",
|
||||||
|
"image": "Imagem",
|
||||||
|
"link": "Link",
|
||||||
|
"vacuum": "Compactar (vacuum)",
|
||||||
|
"select": "Selecionar",
|
||||||
|
"database": "Banco de dados",
|
||||||
|
"upload": "Carregar",
|
||||||
|
"preview": "Pré-visualizar",
|
||||||
|
"rename": "Renomear",
|
||||||
|
"clear": "Limpar",
|
||||||
|
"name": "Nome",
|
||||||
|
"visibility": "Visibilidade",
|
||||||
|
"learn-more": "Saiba mais",
|
||||||
|
"e.g": "ex.",
|
||||||
|
"beta": "Beta"
|
||||||
|
},
|
||||||
|
"router": {
|
||||||
|
"back-to-home": "Voltar ao início"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"signup-as-host": "Registrar como Host",
|
||||||
|
"host-tip": "Você está se registrando como Host do Site.",
|
||||||
|
"not-host-tip": "Se você não tem uma conta, por favor, entre em contato com o Host do site.",
|
||||||
|
"new-password": "Nova senha",
|
||||||
|
"repeat-new-password": "Repita a nova senha"
|
||||||
|
},
|
||||||
|
"editor": {
|
||||||
|
"editing": "Editando...",
|
||||||
|
"cancel-edit": "Cancelar edição",
|
||||||
|
"save": "Salvar",
|
||||||
|
"placeholder": "Alguma ideia...",
|
||||||
|
"only-image-supported": "Apenas imagens são suportadas",
|
||||||
|
"cant-empty": "O conteúdo não pode estar vazio",
|
||||||
|
"local": "Local",
|
||||||
|
"resources": "Recursos"
|
||||||
|
},
|
||||||
|
"memo": {
|
||||||
|
"view-detail": "Ver detalhes",
|
||||||
|
"copy-link": "Copiar link",
|
||||||
|
"embed": "Incorporar",
|
||||||
|
"archived-memos": "Memos arquivados",
|
||||||
|
"no-archived-memos": "Nenhum memo arquivado",
|
||||||
|
"fetching-data": "obtendo dados...",
|
||||||
|
"fetch-more": "Clique para carregar mais dados",
|
||||||
|
"archived-at": "Arquivado em",
|
||||||
|
"search-placeholder": "Pesquisar memos",
|
||||||
|
"visibility": {
|
||||||
|
"private": "Privado (eu)",
|
||||||
|
"protected": "Protegido (membros)",
|
||||||
|
"public": "Público (todos)",
|
||||||
|
"disabled": "Memos públicos estão desabilitados"
|
||||||
|
},
|
||||||
|
"delete-memo": "Deletar memo",
|
||||||
|
"delete-confirm": "Tem certeza de que deseja deletar este memo?\n\nESTA AÇÃO É IRREVERSÍVEL❗"
|
||||||
|
},
|
||||||
|
"resource": {
|
||||||
|
"no-resources": "Nenhum recurso",
|
||||||
|
"fetching-data": "obtendo dados...",
|
||||||
|
"copy-link": "Copiar link",
|
||||||
|
"reset-link": "Redefinir link",
|
||||||
|
"reset-resource-link": "Redefinir link do recurso",
|
||||||
|
"reset-link-prompt": "Tem certeza de que deseja redefinir o link?\nIsso quebrará todos os vínculos atuais.\n\nESTA AÇÃO É IRREVERSÍVEL❗",
|
||||||
|
"delete-resource": "Deletar recurso",
|
||||||
|
"linked-amount": "Quantidade de vínculos",
|
||||||
|
"warning-text": "Tem certeza de que deseja deletar este recurso?\n\nESTA AÇÃO É IRREVERSÍVEL❗",
|
||||||
|
"warning-text-unused": "Tem certeza de que deseja deletar este recurso não utilizado?\n\nESTA AÇÃO É IRREVERSÍVEL❗",
|
||||||
|
"no-unused-resources": "Nenhum recurso não utilizado",
|
||||||
|
"delete-selected-resources": "Deletar recursos selecionados",
|
||||||
|
"no-files-selected": "Nenhum arquivo selecionado❗",
|
||||||
|
"upload-successfully": "Carregado com êxito",
|
||||||
|
"file-drag-drop-prompt": "Arraste e solte o arquivo aqui para carregá-lo",
|
||||||
|
"search-bar-placeholder": "Pesquisar recurso",
|
||||||
|
"create-dialog": {
|
||||||
|
"title": "Criar recurso",
|
||||||
|
"upload-method": "Método de carregamento",
|
||||||
|
"local-file": {
|
||||||
|
"option": "Arquivo local",
|
||||||
|
"choose": "Escolher arquivo"
|
||||||
|
},
|
||||||
|
"external-link": {
|
||||||
|
"option": "Link externo",
|
||||||
|
"link": "Link",
|
||||||
|
"link-placeholder": "https://o.link.para/seu/recurso",
|
||||||
|
"file-name": "Nome",
|
||||||
|
"file-name-placeholder": "Nome do arquivo",
|
||||||
|
"type": "Tipo",
|
||||||
|
"type-placeholder": "Tipo do arquivo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shortcut-list": {
|
||||||
|
"shortcut-title": "título do atalho",
|
||||||
|
"create-shortcut": "Criar atalho",
|
||||||
|
"edit-shortcut": "Editar atalho",
|
||||||
|
"eligible-memo": "memo elegível",
|
||||||
|
"fill-previous": "Por favor, preencha o valor do filtro anterior",
|
||||||
|
"title-required": "O título do atalho é obrigatório",
|
||||||
|
"value-required": "O valor do filtro é obrigatório"
|
||||||
|
},
|
||||||
|
"tag-list": {
|
||||||
|
"tip-text": "Insira `#tag` para criar uma nova",
|
||||||
|
"create-tag": "Criar Tag",
|
||||||
|
"all-tags": "Todas as Tags",
|
||||||
|
"tag-name": "NOME_DA_TAG"
|
||||||
|
},
|
||||||
|
"daily-review": {
|
||||||
|
"title": "Resumo Diário",
|
||||||
|
"no-memos": "Não há memos"
|
||||||
|
},
|
||||||
|
"setting": {
|
||||||
|
"my-account": "Minha conta",
|
||||||
|
"preference": "Preferências",
|
||||||
|
"member": "Membro",
|
||||||
|
"member-list": "Lista de membros",
|
||||||
|
"system": "Sistema",
|
||||||
|
"storage": "Armazenamento",
|
||||||
|
"sso": "SSO",
|
||||||
|
"account-section": {
|
||||||
|
"title": "Informação da conta",
|
||||||
|
"username-note": "Usado para entrar",
|
||||||
|
"nickname-note": "Exibido no banner",
|
||||||
|
"email-note": "Opcional",
|
||||||
|
"update-information": "Atualizar informações",
|
||||||
|
"change-password": "Alterar senha",
|
||||||
|
"reset-api": "Redefinir API",
|
||||||
|
"openapi-title": "OpenAPI",
|
||||||
|
"openapi-reset": "Redefinir chave OpenAPI",
|
||||||
|
"openapi-reset-warning": "❗ A chave de API existente será invalidada e uma nova será gerada.\n\nTem certeza de que deseja redefinir?",
|
||||||
|
"openapi-sample-post": "Olá #memos em {{url}}"
|
||||||
|
},
|
||||||
|
"preference-section": {
|
||||||
|
"theme": "Tema",
|
||||||
|
"default-memo-visibility": "Visibilidade padrão do memo",
|
||||||
|
"default-resource-visibility": "Visibilidade padrão do recurso",
|
||||||
|
"enable-folding-memo": "Habilitar recolher memo",
|
||||||
|
"enable-double-click": "Habilitar duplo clique para editar",
|
||||||
|
"editor-font-style": "Estilo de fonte do editor",
|
||||||
|
"mobile-editor-style": "Estilo do editor para dispositivos móveis",
|
||||||
|
"default-memo-sort-option": "Memo display time",
|
||||||
|
"created_ts": "Hora de criação",
|
||||||
|
"updated_ts": "Hora de atualização",
|
||||||
|
"daily-review-time-offset": "Compensação de tempo do Resumo Diário"
|
||||||
|
},
|
||||||
|
"storage-section": {
|
||||||
|
"current-storage": "Armazenamento atual",
|
||||||
|
"type-database": "Banco de dados",
|
||||||
|
"type-local": "Local",
|
||||||
|
"storage-services-list": "Lista de serviços de armazenamento",
|
||||||
|
"create-a-service": "Criar um serviço",
|
||||||
|
"update-a-service": "Atualizar um serviço",
|
||||||
|
"warning-text": "Tem certeza de que deseja deletar o serviço de armazenamento \"{{name}}\"?\n\nESTA AÇÃO É IRREVERSÍVEL❗",
|
||||||
|
"delete-storage": "Deletar armazenamento",
|
||||||
|
"local-storage-path": "Caminho do armazenamento local",
|
||||||
|
"update-local-path": "Atualizar caminho do armazenamento local",
|
||||||
|
"update-local-path-description": "O caminho de armazenamento local é relativo ao seu banco de dados",
|
||||||
|
"create-storage": "Criar armazenamento",
|
||||||
|
"update-storage": "Atualizar armazenamento",
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"s3-compatible-url": "URL compatível com S3",
|
||||||
|
"region": "Região",
|
||||||
|
"region-placeholder": "Nome da região",
|
||||||
|
"accesskey": "Chave de acesso",
|
||||||
|
"accesskey-placeholder": "Chave de acesso / ID de acesso",
|
||||||
|
"secretkey": "Chave secreta",
|
||||||
|
"secretkey-placeholder": "Chave secreta / Chave de acesso",
|
||||||
|
"bucket": "Bucket",
|
||||||
|
"bucket-placeholder": "Nome do bucket",
|
||||||
|
"path": "Caminho do armazenamento",
|
||||||
|
"path-description": "Você pode usar as mesmas variáveis dinâmicas do armazenamento local, como {filename}",
|
||||||
|
"path-placeholder": "caminho/personalizado",
|
||||||
|
"url-prefix": "Prefixo da URL",
|
||||||
|
"url-prefix-placeholder": "Prefixo personalizado da URL, opcional",
|
||||||
|
"url-suffix": "Sufixo da URL",
|
||||||
|
"url-suffix-placeholder": "Sufixo personalizado da URL, opcional"
|
||||||
|
},
|
||||||
|
"member-section": {
|
||||||
|
"create-a-member": "Criar um membro",
|
||||||
|
"archive-member": "Arquivar membro",
|
||||||
|
"archive-warning": "❗ Tem certeza de que deseja arquivar {{username}}?",
|
||||||
|
"delete-member": "Deletar membro",
|
||||||
|
"delete-warning": "❗ Tem certeza de que deseja deletar {{username}}?\n\nESTA AÇÃO É IRREVERSÍVEL❗"
|
||||||
|
},
|
||||||
|
"system-section": {
|
||||||
|
"server-name": "Nome do servidor",
|
||||||
|
"customize-server": {
|
||||||
|
"title": "Personalizar Servidor",
|
||||||
|
"default": "O padrão é memos",
|
||||||
|
"icon-url": "URL do ícone",
|
||||||
|
"description": "Descrição",
|
||||||
|
"locale": "Localização do servidor",
|
||||||
|
"appearance": "Aparência do servidor"
|
||||||
|
},
|
||||||
|
"database-file-size": "Tamanho do banco de dados",
|
||||||
|
"allow-user-signup": "Permitir registro de usuário",
|
||||||
|
"ignore-version-upgrade": "Ignorar atualização de versão",
|
||||||
|
"disable-public-memos": "Desabilitar memos públicos",
|
||||||
|
"additional-style": "Estilo adicional",
|
||||||
|
"additional-script": "Script adicional",
|
||||||
|
"additional-style-placeholder": "Código CSS adicional",
|
||||||
|
"additional-script-placeholder": "Código JavaScript adicional",
|
||||||
|
"openai-api-key": "OpenAI: Chave de API",
|
||||||
|
"openai-api-key-description": "Obter chave de API",
|
||||||
|
"openai-api-key-placeholder": "Sua chave de API da OpenAI",
|
||||||
|
"openai-api-host": "OpenAI: Host de API",
|
||||||
|
"openai-api-host-placeholder": "Padrão: https://api.openai.com/"
|
||||||
|
},
|
||||||
|
"appearance-option": {
|
||||||
|
"system": "Sistema",
|
||||||
|
"light": "Claro",
|
||||||
|
"dark": "Escuro"
|
||||||
|
},
|
||||||
|
"sso-section": {
|
||||||
|
"sso-list": "Lista de SSOs (Login Único)",
|
||||||
|
"delete-sso": "Confirmar exclusão",
|
||||||
|
"confirm-delete": "Tem certeza de que deseja excluir a configuração SSO \"{{name}}\"?\n\nESTA AÇÃO É IRREVERSÍVEL❗",
|
||||||
|
"create-sso": "Criar SSO",
|
||||||
|
"update-sso": "Atualizar SSO",
|
||||||
|
"sso-created": "SSO {{name}} criado",
|
||||||
|
"sso-updated": "SSO {{name}} atualizado",
|
||||||
|
"identifier": "Identificador",
|
||||||
|
"display-name": "Nome de exibição",
|
||||||
|
"template": "Modelo",
|
||||||
|
"custom": "Personalizado",
|
||||||
|
"identifier-filter": "Filtro identificador",
|
||||||
|
"redirect-url": "URL de redirecionamento",
|
||||||
|
"client-id": "ID do cliente",
|
||||||
|
"client-secret": "Segredo do cliente",
|
||||||
|
"authorization-endpoint": "Endpoint de autenticação",
|
||||||
|
"token-endpoint": "Endpoint de token",
|
||||||
|
"user-endpoint": "Endpoint do usuário",
|
||||||
|
"scopes": "Escopos"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"new-filter": "Novo filtro",
|
||||||
|
"type": {
|
||||||
|
"tag": "Tag",
|
||||||
|
"type": "Tipo",
|
||||||
|
"text": "Texto",
|
||||||
|
"display-time": "Hora de exibição",
|
||||||
|
"visibility": "Visibilidade"
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
"contains": "Contém",
|
||||||
|
"not-contains": "Não contém",
|
||||||
|
"is": "É",
|
||||||
|
"is-not": "Não É",
|
||||||
|
"before": "Antes",
|
||||||
|
"after": "Depois"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"not-tagged": "Sem tags",
|
||||||
|
"linked": "Tem links",
|
||||||
|
"has-attachment": "Tem anexos"
|
||||||
|
},
|
||||||
|
"text-placeholder": "Inicie com ^ para usar regex",
|
||||||
|
"and": "E",
|
||||||
|
"or": "Ou"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"no-memos": "nenhum memo 🌃",
|
||||||
|
"memos-ready": "todos os memos estão prontos 🎉",
|
||||||
|
"no-resource": "nenhum recurso 🌃",
|
||||||
|
"resource-ready": "todos os recursos estão prontos 🎉",
|
||||||
|
"restored-successfully": "Restaurado com êxito",
|
||||||
|
"memo-updated-datetime": "Data do memo atualizada",
|
||||||
|
"invalid-created-datetime": "Data de criação inválida",
|
||||||
|
"change-memo-created-time": "Alterar data de criação do memo",
|
||||||
|
"change-memo-created-time-warning-1": "ESTE NÃO É UM PROCEDIMENTO RECOMENDADO.",
|
||||||
|
"change-memo-created-time-warning-2": "Esteja certo de que realmente precisa disso.",
|
||||||
|
"memo-not-found": "Memo não encontrado",
|
||||||
|
"fill-all": "Por favor, preencha todos os campos",
|
||||||
|
"password-not-match": "As senhas não coincidem",
|
||||||
|
"new-password-not-match": "As novas senhas não coincidem",
|
||||||
|
"image-load-failed": "Falha ao carregar a imagem",
|
||||||
|
"fill-form": "Por favor, preencha o formulário",
|
||||||
|
"fill-server-name": "Por favor, preencha o nome do servidor",
|
||||||
|
"login-failed": "Falha no login",
|
||||||
|
"signup-failed": "Falha no registro",
|
||||||
|
"user-not-found": "Usuário não encontrado",
|
||||||
|
"password-changed": "Senha alterada",
|
||||||
|
"private-only": "Este memo é privado",
|
||||||
|
"copied": "Copiado",
|
||||||
|
"succeed-copy-content": "Conteúdo copiado com êxito.",
|
||||||
|
"succeed-copy-code": "Código copiado com êxito.",
|
||||||
|
"succeed-copy-link": "Link copiado com êxito.",
|
||||||
|
"change-resource-filename": "Alterar nome do arquivo do recurso",
|
||||||
|
"resource-filename-updated": "Nome do arquivo do recurso atualizado",
|
||||||
|
"invalid-resource-filename": "Nome de arquivo de recurso inválido",
|
||||||
|
"click-to-save-the-image": "Clique para salvar a imagem",
|
||||||
|
"generating-the-screenshot": "Gerando a captura de tela...",
|
||||||
|
"count-selected-resources": "Total selecionado",
|
||||||
|
"too-short": "Muito curto",
|
||||||
|
"too-long": "Muito longo",
|
||||||
|
"not-allow-space": "Espaços não são permitidos",
|
||||||
|
"not-allow-chinese": "Caracteres chineses não são permitidos",
|
||||||
|
"succeed-vacuum-database": "Banco de dados compactado com êxito.",
|
||||||
|
"succeed-update-additional-style": "Folha de estilos adicional atualizada com êxito.",
|
||||||
|
"succeed-copy-resource-link": "Link do recurso copiado com êxito.",
|
||||||
|
"succeed-update-customized-profile": "Perfil personalizado com êxito.",
|
||||||
|
"succeed-update-additional-script": "Script adicional atualizado com êxito.",
|
||||||
|
"update-succeed": "Atualizado com êxito",
|
||||||
|
"page-not-found": "404 - Página não encontrada 😥"
|
||||||
|
},
|
||||||
|
"days": {
|
||||||
|
"mon": "Seg",
|
||||||
|
"tue": "Ter",
|
||||||
|
"wed": "Qua",
|
||||||
|
"thu": "Qui",
|
||||||
|
"fri": "Sex",
|
||||||
|
"sat": "Sáb",
|
||||||
|
"sun": "Dom"
|
||||||
|
},
|
||||||
|
"ask-ai": {
|
||||||
|
"title": "Pergunte a IA",
|
||||||
|
"not-enabled": "Você não configurou a chave de API da OpenAI.",
|
||||||
|
"go-to-settings": "Ir para configurações",
|
||||||
|
"placeholder": "Pergunte qualquer coisa para a IA..."
|
||||||
|
},
|
||||||
|
"embed-memo": {
|
||||||
|
"title": "Incorporar memo",
|
||||||
|
"text": "Copie o código abaixo para incorporar este memo em seu site.",
|
||||||
|
"only-public-supported": "* Somente memos públicos podem ser incorporados.",
|
||||||
|
"copy": "Copiar"
|
||||||
|
},
|
||||||
|
"heatmap": {
|
||||||
|
"memo-in": "{{amount}} memo em {{period}}",
|
||||||
|
"memos-in": "{{amount}} memos em {{period}}",
|
||||||
|
"memo-on": "{{amount}} memo em {{date}}",
|
||||||
|
"memos-on": "{{amount}} memos em {{date}}",
|
||||||
|
"day": "dia",
|
||||||
|
"days": "dias"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"about-memos": "Sobre o Memos",
|
||||||
|
"memos-description": "Memos é um aplicativo de bloco de notas baseado na web que você pode usar para escrever, organizar e compartilhar anotações.",
|
||||||
|
"no-server-description": "Nenhuma descrição configurada para este servidor.",
|
||||||
|
"powered-by": "Provido por",
|
||||||
|
"other-projects": "Outros projetos"
|
||||||
|
}
|
||||||
|
}
|
@ -1,253 +0,0 @@
|
|||||||
{
|
|
||||||
"common": {
|
|
||||||
"about": "Sobre",
|
|
||||||
"email": "E-mail",
|
|
||||||
"password": "Senha",
|
|
||||||
"repeat-password-short": "Repita",
|
|
||||||
"repeat-password": "Repita a senha",
|
|
||||||
"new-password": "Nova senha",
|
|
||||||
"repeat-new-password": "Repita a nova senha",
|
|
||||||
"avatar": "Avatar",
|
|
||||||
"username": "Nome de usuário",
|
|
||||||
"nickname": "Apelido",
|
|
||||||
"save": "Salvar",
|
|
||||||
"close": "Fechar",
|
|
||||||
"cancel": "Cancelar",
|
|
||||||
"create": "Criar",
|
|
||||||
"change": "Alterar",
|
|
||||||
"confirm": "Confirmar",
|
|
||||||
"reset": "Redefinir",
|
|
||||||
"language": "Idioma",
|
|
||||||
"version": "Versão",
|
|
||||||
"pin": "Fixar",
|
|
||||||
"unpin": "Desfixar",
|
|
||||||
"edit": "Editar",
|
|
||||||
"restore": "Restaurar",
|
|
||||||
"delete": "Apagar",
|
|
||||||
"null": "Nulo",
|
|
||||||
"share": "Compartilhar",
|
|
||||||
"archive": "Arquivar",
|
|
||||||
"basic": "Básico",
|
|
||||||
"admin": "Admin",
|
|
||||||
"explore": "Explorar",
|
|
||||||
"sign-in": "Entrar",
|
|
||||||
"sign-up": "Cadastrar-se",
|
|
||||||
"sign-out": "Deslogar",
|
|
||||||
"back-to-home": "Voltar para Início",
|
|
||||||
"type": "Type",
|
|
||||||
"shortcuts": "Atalhos",
|
|
||||||
"title": "Título",
|
|
||||||
"filter": "Filtro",
|
|
||||||
"tags": "Tags",
|
|
||||||
"yourself": "Você mesmo",
|
|
||||||
"archived-at": "Arquivado em",
|
|
||||||
"changed": "alterado",
|
|
||||||
"update-on": "Update on",
|
|
||||||
"fold": "Fold",
|
|
||||||
"expand": "Expandir",
|
|
||||||
"image": "Imagem",
|
|
||||||
"link": "Link",
|
|
||||||
"vacuum": "Vacuum",
|
|
||||||
"select": "Selecionar",
|
|
||||||
"database": "Banco de dados"
|
|
||||||
},
|
|
||||||
"auth": {
|
|
||||||
"signup-as-host": "Sign up as Host",
|
|
||||||
"host-tip": "You are registering as the Site Host.",
|
|
||||||
"not-host-tip": "If you don't have an account, please contact the site host."
|
|
||||||
},
|
|
||||||
"sidebar": {
|
|
||||||
"daily-review": "Revisão diária",
|
|
||||||
"resources": "Recursos",
|
|
||||||
"setting": "Configurações",
|
|
||||||
"archived": "Arquivado"
|
|
||||||
},
|
|
||||||
"resource": {
|
|
||||||
"description": "View your static resources in memos. e.g. images",
|
|
||||||
"no-resources": "No resources.",
|
|
||||||
"fetching-data": "fetching data...",
|
|
||||||
"upload": "Enviar",
|
|
||||||
"preview": "Pré-visualizar",
|
|
||||||
"warning-text": "Are you sure to delete this resource? THIS ACTION IS IRREVERSIBLE❗",
|
|
||||||
"copy-link": "Copiar Link",
|
|
||||||
"delete-resource": "Delete Resource",
|
|
||||||
"linked-amount": "Linked memo amount",
|
|
||||||
"rename": "Renomear",
|
|
||||||
"clear": "Limpar",
|
|
||||||
"warning-text-unused": "Are you sure to delete these unused resource? THIS ACTION IS IRREVERSIBLE❗",
|
|
||||||
"no-unused-resources": "No unused resources",
|
|
||||||
"name": "Nome"
|
|
||||||
},
|
|
||||||
"archived": {
|
|
||||||
"archived-memos": "Memos Arquivados",
|
|
||||||
"no-archived-memos": "No archived memos.",
|
|
||||||
"fetching-data": "fetching data..."
|
|
||||||
},
|
|
||||||
"editor": {
|
|
||||||
"editing": "Editando...",
|
|
||||||
"cancel-edit": "Cancelar edição",
|
|
||||||
"save": "Salvar",
|
|
||||||
"placeholder": "Any thoughts...",
|
|
||||||
"only-image-supported": "Only image file supported.",
|
|
||||||
"cant-empty": "Content can't be empty",
|
|
||||||
"local": "Local",
|
|
||||||
"resources": "Resources"
|
|
||||||
},
|
|
||||||
"memo": {
|
|
||||||
"view-detail": "View Detail",
|
|
||||||
"copy": "Copy",
|
|
||||||
"copy-link": "Copiar Link",
|
|
||||||
"visibility": {
|
|
||||||
"private": "Visível apenas para você",
|
|
||||||
"protected": "Visível para membros",
|
|
||||||
"public": "Todos podem ver",
|
|
||||||
"disabled": "Memos públicos estão desativados"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"memo-list": {
|
|
||||||
"fetching-data": "fetching data...",
|
|
||||||
"fetch-more": "Click here to fetch more"
|
|
||||||
},
|
|
||||||
"shortcut-list": {
|
|
||||||
"shortcut-title": "shortcut title",
|
|
||||||
"create-shortcut": "Criar Atalho",
|
|
||||||
"edit-shortcut": "Editar Atalho",
|
|
||||||
"eligible-memo": "eligible memo",
|
|
||||||
"fill-previous": "Please fill in previous filter value",
|
|
||||||
"title-required": "Title is required",
|
|
||||||
"value-required": "Filter value is required"
|
|
||||||
},
|
|
||||||
"filter": {
|
|
||||||
"new-filter": "New Filter",
|
|
||||||
"type": {
|
|
||||||
"tag": "Tag",
|
|
||||||
"type": "Type",
|
|
||||||
"text": "Texto",
|
|
||||||
"display-time": "Display Time",
|
|
||||||
"visibility": "Visibility"
|
|
||||||
},
|
|
||||||
"operator": {
|
|
||||||
"contains": "Contains",
|
|
||||||
"not-contains": "Does not contain",
|
|
||||||
"is": "Is",
|
|
||||||
"is-not": "Is Not",
|
|
||||||
"before": "Before",
|
|
||||||
"after": "After"
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"not-tagged": "No tags",
|
|
||||||
"linked": "Has links"
|
|
||||||
},
|
|
||||||
"text-placeholder": "Starts with ^ to use regex"
|
|
||||||
},
|
|
||||||
"tag-list": {
|
|
||||||
"tip-text": "Input `#tag` to create"
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"quickly-filter": "Quickly filter"
|
|
||||||
},
|
|
||||||
"setting": {
|
|
||||||
"my-account": "My Account",
|
|
||||||
"preference": "Preference",
|
|
||||||
"member": "Member",
|
|
||||||
"member-list": "Member list",
|
|
||||||
"system": "System",
|
|
||||||
"storage": "Storage",
|
|
||||||
"sso": "SSO",
|
|
||||||
"account-section": {
|
|
||||||
"title": "Account Information",
|
|
||||||
"update-information": "Update Information",
|
|
||||||
"change-password": "Change password"
|
|
||||||
},
|
|
||||||
"preference-section": {
|
|
||||||
"theme": "Theme",
|
|
||||||
"default-memo-visibility": "Default memo visibility",
|
|
||||||
"enable-folding-memo": "Enable folding memo",
|
|
||||||
"enable-double-click": "Enable double-click to edit",
|
|
||||||
"editor-font-style": "Editor font style",
|
|
||||||
"mobile-editor-style": "Mobile editor style",
|
|
||||||
"default-memo-sort-option": "Memo display time",
|
|
||||||
"created_ts": "Created Time",
|
|
||||||
"updated_ts": "Updated Time"
|
|
||||||
},
|
|
||||||
"storage-section": {
|
|
||||||
"storage-services-list": "Storage service list",
|
|
||||||
"create-a-service": "Create a service",
|
|
||||||
"update-a-service": "Update a service",
|
|
||||||
"warning-text": "Are you sure to delete this storage service? THIS ACTION IS IRREVERSIBLE❗",
|
|
||||||
"delete-storage": "Delete Storage"
|
|
||||||
},
|
|
||||||
"member-section": {
|
|
||||||
"create-a-member": "Create a member"
|
|
||||||
},
|
|
||||||
"system-section": {
|
|
||||||
"server-name": "Server Name",
|
|
||||||
"customize-server": {
|
|
||||||
"title": "Customize Server",
|
|
||||||
"default": "Default is memos",
|
|
||||||
"icon-url": "Icon URL"
|
|
||||||
},
|
|
||||||
"database-file-size": "Database File Size",
|
|
||||||
"allow-user-signup": "Allow user signup",
|
|
||||||
"disable-public-memos": "Disable public memos",
|
|
||||||
"additional-style": "Additional style",
|
|
||||||
"additional-script": "Additional script",
|
|
||||||
"additional-style-placeholder": "Additional CSS codes",
|
|
||||||
"additional-script-placeholder": "Additional JavaScript codes"
|
|
||||||
},
|
|
||||||
"appearance-option": {
|
|
||||||
"system": "Follow system",
|
|
||||||
"light": "Always light",
|
|
||||||
"dark": "Always dark"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"no-memos": "no memos 🌃",
|
|
||||||
"memos-ready": "all memos are ready 🎉",
|
|
||||||
"restored-successfully": "Restored successfully",
|
|
||||||
"memo-updated-datetime": "Memo created datetime changed.",
|
|
||||||
"invalid-created-datetime": "Invalid created datetime.",
|
|
||||||
"change-memo-created-time": "Change memo created time",
|
|
||||||
"memo-not-found": "Memo not found.",
|
|
||||||
"fill-all": "Please fill in all fields.",
|
|
||||||
"password-not-match": "Passwords do not match.",
|
|
||||||
"new-password-not-match": "New passwords do not match.",
|
|
||||||
"image-load-failed": "Image load failed",
|
|
||||||
"fill-form": "Please fill out this form",
|
|
||||||
"login-failed": "Login failed",
|
|
||||||
"signup-failed": "Signup failed",
|
|
||||||
"user-not-found": "User not found",
|
|
||||||
"password-changed": "Password Changed",
|
|
||||||
"private-only": "This memo is private only.",
|
|
||||||
"copied": "Copied",
|
|
||||||
"succeed-copy-content": "Succeed to copy content to clipboard.",
|
|
||||||
"succeed-copy-code": "Succeed to copy code to clipboard.",
|
|
||||||
"succeed-copy-link": "Succeed to copy link to clipboard.",
|
|
||||||
"change-resource-filename": "Change resource filename",
|
|
||||||
"resource-filename-updated": "Resource filename changed.",
|
|
||||||
"invalid-resource-filename": "Invalid filename.",
|
|
||||||
"click-to-save-the-image": "Click to save the image",
|
|
||||||
"generating-the-screenshot": "Generating the screenshot...",
|
|
||||||
"count-selected-resources": "Total selected",
|
|
||||||
"too-short": "Too short",
|
|
||||||
"too-long": "Too long",
|
|
||||||
"not-allow-space": "Don't allow space",
|
|
||||||
"not-allow-chinese": "Don't allow chinese",
|
|
||||||
"succeed-vacuum-database": "Succeed to vacuum database",
|
|
||||||
"succeed-update-additional-style": "Succeed to update additional style",
|
|
||||||
"succeed-copy-resource-link": "Succeed to copy resource link to clipboard",
|
|
||||||
"succeed-update-customized-profile": "Succeed to update customized profile",
|
|
||||||
"succeed-update-additional-script": "Succeed to update additional script",
|
|
||||||
"update-succeed": "Update succeed",
|
|
||||||
"page-not-found": "404 - Page Not Found 😥"
|
|
||||||
},
|
|
||||||
"days": {
|
|
||||||
"mon": "Mon",
|
|
||||||
"tue": "Tue",
|
|
||||||
"wed": "Wed",
|
|
||||||
"thu": "Thu",
|
|
||||||
"fri": "Fri",
|
|
||||||
"sat": "Sat",
|
|
||||||
"sun": "Sun"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,4 @@
|
|||||||
import { CssVarsProvider } from "@mui/joy";
|
import { CssVarsProvider } from "@mui/joy";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import store from "./store";
|
import store from "./store";
|
||||||
@ -12,13 +10,6 @@ import "./less/code-highlight.less";
|
|||||||
import "./css/global.css";
|
import "./css/global.css";
|
||||||
import "./css/tailwind.css";
|
import "./css/tailwind.css";
|
||||||
|
|
||||||
import "dayjs/locale/zh";
|
|
||||||
import "dayjs/locale/fr";
|
|
||||||
import "dayjs/locale/vi";
|
|
||||||
import "dayjs/locale/ru";
|
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
|
|
||||||
const container = document.getElementById("root");
|
const container = document.getElementById("root");
|
||||||
const root = createRoot(container as HTMLElement);
|
const root = createRoot(container as HTMLElement);
|
||||||
root.render(
|
root.render(
|
||||||
|
@ -4,7 +4,6 @@ import { toast } from "react-hot-toast";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useGlobalStore, useUserStore } from "@/store/module";
|
import { useGlobalStore, useUserStore } from "@/store/module";
|
||||||
import * as api from "@/helpers/api";
|
import * as api from "@/helpers/api";
|
||||||
import { slogan } from "@/helpers/site";
|
|
||||||
import { absolutifyLink } from "@/helpers/utils";
|
import { absolutifyLink } from "@/helpers/utils";
|
||||||
import useLoading from "@/hooks/useLoading";
|
import useLoading from "@/hooks/useLoading";
|
||||||
import Icon from "@/components/Icon";
|
import Icon from "@/components/Icon";
|
||||||
@ -80,7 +79,7 @@ const Auth = () => {
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error(error.response.data.error);
|
toast.error(error.response.data.message || t("message.login-failed"));
|
||||||
}
|
}
|
||||||
actionBtnLoadingState.setFinish();
|
actionBtnLoadingState.setFinish();
|
||||||
};
|
};
|
||||||
@ -101,11 +100,11 @@ const Auth = () => {
|
|||||||
if (user) {
|
if (user) {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
} else {
|
} else {
|
||||||
toast.error(t("common.singup-failed"));
|
toast.error(t("common.signup-failed"));
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error(error.response.data.error);
|
toast.error(error.response.data.message || error.message || t("common.signup-failed"));
|
||||||
}
|
}
|
||||||
actionBtnLoadingState.setFinish();
|
actionBtnLoadingState.setFinish();
|
||||||
};
|
};
|
||||||
@ -133,7 +132,9 @@ const Auth = () => {
|
|||||||
<img className="h-12 w-auto rounded-lg mr-1" src={systemStatus.customizedProfile.logoUrl} alt="" />
|
<img className="h-12 w-auto rounded-lg mr-1" src={systemStatus.customizedProfile.logoUrl} alt="" />
|
||||||
<p className="text-6xl tracking-wide text-black opacity-80 dark:text-gray-200">{systemStatus.customizedProfile.name}</p>
|
<p className="text-6xl tracking-wide text-black opacity-80 dark:text-gray-200">{systemStatus.customizedProfile.name}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-700 dark:text-gray-300">{systemStatus.customizedProfile.description || slogan}</p>
|
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{systemStatus.customizedProfile.description || t("common.memos-slogan")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form className="w-full" onSubmit={handleFormSubmit}>
|
<form className="w-full" onSubmit={handleFormSubmit}>
|
||||||
<div className={`flex flex-col justify-start items-start w-full ${actionBtnLoadingState.isLoading && "opacity-80"}`}>
|
<div className={`flex flex-col justify-start items-start w-full ${actionBtnLoadingState.isLoading && "opacity-80"}`}>
|
||||||
@ -209,7 +210,7 @@ const Auth = () => {
|
|||||||
</form>
|
</form>
|
||||||
{identityProviderList.length > 0 && (
|
{identityProviderList.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Divider className="!my-4">or</Divider>
|
<Divider className="!my-4">{t("common.or")}</Divider>
|
||||||
<div className="w-full flex flex-col space-y-2">
|
<div className="w-full flex flex-col space-y-2">
|
||||||
{identityProviderList.map((identityProvider) => (
|
{identityProviderList.map((identityProvider) => (
|
||||||
<Button
|
<Button
|
||||||
@ -220,7 +221,7 @@ const Auth = () => {
|
|||||||
size="md"
|
size="md"
|
||||||
onClick={() => handleSignInWithIdentityProvider(identityProvider)}
|
onClick={() => handleSignInWithIdentityProvider(identityProvider)}
|
||||||
>
|
>
|
||||||
Sign in with {identityProvider.name}
|
{t("common.sign-in-with", { provider: identityProvider.name })}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,6 @@ import toast from "react-hot-toast";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMemoStore, useUserStore } from "@/store/module";
|
import { useMemoStore, useUserStore } from "@/store/module";
|
||||||
import { DAILY_TIMESTAMP, DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
import { DAILY_TIMESTAMP, DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
||||||
import * as utils from "@/helpers/utils";
|
|
||||||
import MobileHeader from "@/components/MobileHeader";
|
import MobileHeader from "@/components/MobileHeader";
|
||||||
import useToggle from "@/hooks/useToggle";
|
import useToggle from "@/hooks/useToggle";
|
||||||
import toImage from "@/labs/html2image";
|
import toImage from "@/labs/html2image";
|
||||||
@ -12,6 +11,9 @@ import showPreviewImageDialog from "@/components/PreviewImageDialog";
|
|||||||
import Icon from "@/components/Icon";
|
import Icon from "@/components/Icon";
|
||||||
import DatePicker from "@/components/kit/DatePicker";
|
import DatePicker from "@/components/kit/DatePicker";
|
||||||
import DailyMemo from "@/components/DailyMemo";
|
import DailyMemo from "@/components/DailyMemo";
|
||||||
|
import i18n from "@/i18n";
|
||||||
|
import { findNearestLanguageMatch } from "@/utils/i18n";
|
||||||
|
import { convertToMillis, getDateStampByDate, getNormalizedDateString, getTimeStampByDate } from "@/helpers/datetime";
|
||||||
|
|
||||||
const DailyReview = () => {
|
const DailyReview = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -20,21 +22,21 @@ const DailyReview = () => {
|
|||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const { localSetting } = userStore.state.user as User;
|
const { localSetting } = userStore.state.user as User;
|
||||||
const [currentDateStamp, setCurrentDateStamp] = useState(utils.getDateStampByDate(utils.getDateString(Date.now())));
|
const [currentDateStamp, setCurrentDateStamp] = useState(getDateStampByDate(getNormalizedDateString()));
|
||||||
const [showDatePicker, toggleShowDatePicker] = useToggle(false);
|
const [showDatePicker, toggleShowDatePicker] = useToggle(false);
|
||||||
const memosElRef = useRef<HTMLDivElement>(null);
|
const memosElRef = useRef<HTMLDivElement>(null);
|
||||||
const currentDate = new Date(currentDateStamp);
|
const currentDate = new Date(currentDateStamp);
|
||||||
const dailyMemos = memos
|
const dailyMemos = memos
|
||||||
.filter((m) => {
|
.filter((m) => {
|
||||||
const createdTimestamp = utils.getTimeStampByDate(m.createdTs);
|
const createdTimestamp = getTimeStampByDate(m.createdTs);
|
||||||
const currentDateStampWithOffset = currentDateStamp + utils.convertToMillis(localSetting);
|
const currentDateStampWithOffset = currentDateStamp + convertToMillis(localSetting);
|
||||||
return (
|
return (
|
||||||
m.rowStatus === "NORMAL" &&
|
m.rowStatus === "NORMAL" &&
|
||||||
createdTimestamp >= currentDateStampWithOffset &&
|
createdTimestamp >= currentDateStampWithOffset &&
|
||||||
createdTimestamp < currentDateStampWithOffset + DAILY_TIMESTAMP
|
createdTimestamp < currentDateStampWithOffset + DAILY_TIMESTAMP
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.sort((a, b) => utils.getTimeStampByDate(a.createdTs) - utils.getTimeStampByDate(b.createdTs));
|
.sort((a, b) => getTimeStampByDate(a.createdTs) - getTimeStampByDate(b.createdTs));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchMoreMemos = async () => {
|
const fetchMoreMemos = async () => {
|
||||||
@ -77,6 +79,10 @@ const DailyReview = () => {
|
|||||||
toggleShowDatePicker(false);
|
toggleShowDatePicker(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const locale = findNearestLanguageMatch(i18n.language);
|
||||||
|
const currentMonth = currentDate.toLocaleDateString(locale, { month: "short" });
|
||||||
|
const currentDayOfWeek = currentDate.toLocaleDateString(locale, { weekday: "short" });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="w-full max-w-2xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
|
<section className="w-full max-w-2xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
|
||||||
<MobileHeader showSearch={false} />
|
<MobileHeader showSearch={false} />
|
||||||
@ -124,17 +130,17 @@ const DailyReview = () => {
|
|||||||
<div className="mx-auto font-bold text-gray-600 dark:text-gray-300 text-center leading-6 mb-2">{currentDate.getFullYear()}</div>
|
<div className="mx-auto font-bold text-gray-600 dark:text-gray-300 text-center leading-6 mb-2">{currentDate.getFullYear()}</div>
|
||||||
<div className="flex flex-col justify-center items-center m-auto w-24 h-24 shadow rounded-3xl dark:bg-zinc-800">
|
<div className="flex flex-col justify-center items-center m-auto w-24 h-24 shadow rounded-3xl dark:bg-zinc-800">
|
||||||
<div className="text-center w-full leading-6 text-sm text-white bg-blue-700 rounded-t-3xl">
|
<div className="text-center w-full leading-6 text-sm text-white bg-blue-700 rounded-t-3xl">
|
||||||
{currentDate.toLocaleString("en-US", { month: "short" })}
|
{currentMonth[0].toUpperCase() + currentMonth.substring(1)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-black dark:text-white text-4xl font-medium leading-12">{currentDate.getDate()}</div>
|
<div className="text-black dark:text-white text-4xl font-medium leading-12">{currentDate.getDate()}</div>
|
||||||
<div className="dark:text-gray-300 text-center w-full leading-6 -mt-2 text-xs">
|
<div className="dark:text-gray-300 text-center w-full leading-6 -mt-2 text-xs">
|
||||||
{currentDate.toLocaleString("en-US", { weekday: "short" })}
|
{currentDayOfWeek[0].toUpperCase() + currentDayOfWeek.substring(1)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{dailyMemos.length === 0 ? (
|
{dailyMemos.length === 0 ? (
|
||||||
<div className="mx-auto pt-4 pb-5 px-0">
|
<div className="mx-auto pt-4 pb-5 px-0">
|
||||||
<p className="italic text-gray-400">Oops, there is nothing.</p>
|
<p className="italic text-gray-400">{t("daily-review.no-memos")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col justify-start items-start w-full mt-2">
|
<div className="flex flex-col justify-start items-start w-full mt-2">
|
||||||
|
@ -1,20 +1,18 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { UNKNOWN_ID } from "@/helpers/consts";
|
import { UNKNOWN_ID } from "@/helpers/consts";
|
||||||
import { useMemoStore } from "@/store/module";
|
import { useMemoStore } from "@/store/module";
|
||||||
import useLoading from "@/hooks/useLoading";
|
import useLoading from "@/hooks/useLoading";
|
||||||
import MemoContent from "@/components/MemoContent";
|
import MemoContent from "@/components/MemoContent";
|
||||||
import MemoResources from "@/components/MemoResources";
|
import MemoResources from "@/components/MemoResources";
|
||||||
|
import { getDateTimeString } from "@/helpers/datetime";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
memo: Memo;
|
memo: Memo;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmbedMemo = () => {
|
const EmbedMemo = () => {
|
||||||
const { i18n } = useTranslation();
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const memoStore = useMemoStore();
|
const memoStore = useMemoStore();
|
||||||
const [state, setState] = useState<State>({
|
const [state, setState] = useState<State>({
|
||||||
@ -47,7 +45,7 @@ const EmbedMemo = () => {
|
|||||||
<main className="w-full max-w-lg mx-auto my-auto shadow px-4 py-4 rounded-lg">
|
<main className="w-full max-w-lg mx-auto my-auto shadow px-4 py-4 rounded-lg">
|
||||||
<div className="w-full flex flex-col justify-start items-start">
|
<div className="w-full flex flex-col justify-start items-start">
|
||||||
<div className="w-full mb-2 flex flex-row justify-start items-center text-sm text-gray-400 dark:text-gray-300">
|
<div className="w-full mb-2 flex flex-row justify-start items-center text-sm text-gray-400 dark:text-gray-300">
|
||||||
<span>{dayjs(state.memo.createdTs).locale(i18n.language).format("YYYY/MM/DD HH:mm:ss")}</span>
|
<span>{getDateTimeString(state.memo.createdTs)}</span>
|
||||||
<a className="ml-2 hover:underline hover:text-green-600" href={`/u/${state.memo.creatorId}`}>
|
<a className="ml-2 hover:underline hover:text-green-600" href={`/u/${state.memo.creatorId}`}>
|
||||||
@{state.memo.creatorName}
|
@{state.memo.creatorName}
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -8,6 +7,7 @@ import { useGlobalStore, useMemoStore, useUserStore } from "@/store/module";
|
|||||||
import useLoading from "@/hooks/useLoading";
|
import useLoading from "@/hooks/useLoading";
|
||||||
import MemoContent from "@/components/MemoContent";
|
import MemoContent from "@/components/MemoContent";
|
||||||
import MemoResources from "@/components/MemoResources";
|
import MemoResources from "@/components/MemoResources";
|
||||||
|
import { getDateTimeString } from "@/helpers/datetime";
|
||||||
import "@/less/memo-detail.less";
|
import "@/less/memo-detail.less";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -15,7 +15,7 @@ interface State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MemoDetail = () => {
|
const MemoDetail = () => {
|
||||||
const { t, i18n } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const globalStore = useGlobalStore();
|
const globalStore = useGlobalStore();
|
||||||
@ -77,7 +77,7 @@ const MemoDetail = () => {
|
|||||||
<div className="memo-container">
|
<div className="memo-container">
|
||||||
<div className="memo-header">
|
<div className="memo-header">
|
||||||
<div className="status-container">
|
<div className="status-container">
|
||||||
<span className="time-text">{dayjs(state.memo.createdTs).locale(i18n.language).format("YYYY/MM/DD HH:mm:ss")}</span>
|
<span className="time-text">{getDateTimeString(state.memo.createdTs)}</span>
|
||||||
<a className="name-text" href={`/u/${state.memo.creatorId}`}>
|
<a className="name-text" href={`/u/${state.memo.creatorId}`}>
|
||||||
@{state.memo.creatorName}
|
@{state.memo.creatorName}
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as api from "@/helpers/api";
|
import * as api from "@/helpers/api";
|
||||||
import * as storage from "@/helpers/storage";
|
import * as storage from "@/helpers/storage";
|
||||||
import i18n from "@/i18n";
|
import i18n from "@/i18n";
|
||||||
import { convertLanguageCodeToLocale } from "@/utils/i18n";
|
import { findNearestLanguageMatch } from "@/utils/i18n";
|
||||||
import store, { useAppSelector } from "../";
|
import store, { useAppSelector } from "../";
|
||||||
import { setAppearance, setGlobalState, setLocale } from "../reducer/global";
|
import { setAppearance, setGlobalState, setLocale } from "../reducer/global";
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ export const initialGlobalState = async () => {
|
|||||||
externalUrl: "",
|
externalUrl: "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
defaultGlobalState.locale = storageLocale || convertLanguageCodeToLocale(i18n.language);
|
defaultGlobalState.locale = storageLocale || findNearestLanguageMatch(i18n.language);
|
||||||
defaultGlobalState.appearance = customizedProfile.appearance;
|
defaultGlobalState.appearance = customizedProfile.appearance;
|
||||||
}
|
}
|
||||||
store.dispatch(setGlobalState(defaultGlobalState));
|
store.dispatch(setGlobalState(defaultGlobalState));
|
||||||
|
2
web/src/types/i18n.d.ts
vendored
2
web/src/types/i18n.d.ts
vendored
@ -1 +1 @@
|
|||||||
type Locale = "en" | "zh" | "vi" | "fr" | "nl" | "sv" | "de" | "es" | "uk" | "ru" | "it" | "hant" | "tr" | "ko" | "sl";
|
type Locale = string;
|
||||||
|
@ -1,7 +1,35 @@
|
|||||||
export const convertLanguageCodeToLocale = (codename: string): Locale => {
|
import i18n, { TLocale, availableLocales } from "@/i18n";
|
||||||
if (codename === "zh-TW" || codename === "zh-HK") {
|
import { FallbackLngObjList } from "i18next";
|
||||||
return "hant";
|
|
||||||
|
export const findNearestLanguageMatch = (codename: string): Locale => {
|
||||||
|
// Find existing translations for full codes (e.g. "en-US", "zh-Hant")
|
||||||
|
if (codename.length > 2 && availableLocales.includes(codename as TLocale)) {
|
||||||
|
return codename as Locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find fallback in src/i18n.ts
|
||||||
|
const i18nfallbacks = Object.entries(i18n.store.options.fallbackLng as FallbackLngObjList);
|
||||||
|
for (const [main, fallbacks] of i18nfallbacks) {
|
||||||
|
if (codename === main) {
|
||||||
|
return fallbacks[0] as Locale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const shortCode = codename.substring(0, 2);
|
const shortCode = codename.substring(0, 2);
|
||||||
|
|
||||||
|
// Match existing short code translation
|
||||||
|
if (availableLocales.includes(shortCode as TLocale)) {
|
||||||
return shortCode as Locale;
|
return shortCode as Locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to match "xx-YY" to existing translation for "xx-ZZ" as a last resort
|
||||||
|
// If some match is undesired, it can be overriden in src/i18n.ts `fallbacks` option
|
||||||
|
for (const existing of availableLocales) {
|
||||||
|
if (shortCode == existing.substring(0, 2)) {
|
||||||
|
return existing as Locale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// should be "en", so the selector is not empty if there isn't a translation for current user's language
|
||||||
|
return (i18n.store.options.fallbackLng as FallbackLngObjList).default[0] as Locale;
|
||||||
};
|
};
|
||||||
|
8983
web/yarn.lock
8983
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user