mirror of
https://github.com/usememos/memos.git
synced 2024-09-21 08:49:39 +03:00
feat: customize system profile (#774)
* feat: system setting for customized profile * chore: update * feat: update frontend * chore: update
This commit is contained in:
parent
55695f2189
commit
b67ed1ee13
@ -14,4 +14,6 @@ type SystemStatus struct {
|
|||||||
AdditionalStyle string `json:"additionalStyle"`
|
AdditionalStyle string `json:"additionalStyle"`
|
||||||
// Additional script.
|
// Additional script.
|
||||||
AdditionalScript string `json:"additionalScript"`
|
AdditionalScript string `json:"additionalScript"`
|
||||||
|
// Customized server profile, including server name and external url.
|
||||||
|
CustomizedProfile CustomizedProfile `json:"customizedProfile"`
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,20 @@ const (
|
|||||||
SystemSettingAdditionalStyleName SystemSettingName = "additionalStyle"
|
SystemSettingAdditionalStyleName SystemSettingName = "additionalStyle"
|
||||||
// SystemSettingAdditionalScriptName is the key type of additional script.
|
// SystemSettingAdditionalScriptName is the key type of additional script.
|
||||||
SystemSettingAdditionalScriptName SystemSettingName = "additionalScript"
|
SystemSettingAdditionalScriptName SystemSettingName = "additionalScript"
|
||||||
|
// SystemSettingCustomizedProfileName is the key type of customized server profile.
|
||||||
|
SystemSettingCustomizedProfileName SystemSettingName = "customizedProfile"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CustomizedProfile is the struct definition for SystemSettingCustomizedProfileName system setting item.
|
||||||
|
type CustomizedProfile struct {
|
||||||
|
// Name is the server name, default is `memos`
|
||||||
|
Name string `json:"name"`
|
||||||
|
// IconURL is the url of icon image.
|
||||||
|
IconURL string `json:"iconUrl"`
|
||||||
|
// ExternalURL is the external url of server. e.g. https://usermemos.com
|
||||||
|
ExternalURL string `json:"externalUrl"`
|
||||||
|
}
|
||||||
|
|
||||||
func (key SystemSettingName) String() string {
|
func (key SystemSettingName) String() string {
|
||||||
switch key {
|
switch key {
|
||||||
case SystemSettingAllowSignUpName:
|
case SystemSettingAllowSignUpName:
|
||||||
@ -24,6 +36,8 @@ func (key SystemSettingName) String() string {
|
|||||||
return "additionalStyle"
|
return "additionalStyle"
|
||||||
case SystemSettingAdditionalScriptName:
|
case SystemSettingAdditionalScriptName:
|
||||||
return "additionalScript"
|
return "additionalScript"
|
||||||
|
case SystemSettingCustomizedProfileName:
|
||||||
|
return "customizedProfile"
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@ -75,6 +89,16 @@ func (upsert SystemSettingUpsert) Validate() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to unmarshal system setting additional script value")
|
return fmt.Errorf("failed to unmarshal system setting additional script value")
|
||||||
}
|
}
|
||||||
|
} else if upsert.Name == SystemSettingCustomizedProfileName {
|
||||||
|
value := CustomizedProfile{
|
||||||
|
Name: "memos",
|
||||||
|
IconURL: "",
|
||||||
|
ExternalURL: "",
|
||||||
|
}
|
||||||
|
err := json.Unmarshal([]byte(upsert.Value), &value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal system setting customized profile value")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("invalid system setting name")
|
return fmt.Errorf("invalid system setting name")
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,9 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
|||||||
AllowSignUp: false,
|
AllowSignUp: false,
|
||||||
AdditionalStyle: "",
|
AdditionalStyle: "",
|
||||||
AdditionalScript: "",
|
AdditionalScript: "",
|
||||||
|
CustomizedProfile: api.CustomizedProfile{
|
||||||
|
Name: "memos",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
|
systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
|
||||||
@ -54,7 +57,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
|||||||
}
|
}
|
||||||
for _, systemSetting := range systemSettingList {
|
for _, systemSetting := range systemSettingList {
|
||||||
var value interface{}
|
var value interface{}
|
||||||
err = json.Unmarshal([]byte(systemSetting.Value), &value)
|
err := json.Unmarshal([]byte(systemSetting.Value), &value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
|
||||||
}
|
}
|
||||||
@ -65,6 +68,13 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
|||||||
systemStatus.AdditionalStyle = value.(string)
|
systemStatus.AdditionalStyle = value.(string)
|
||||||
} else if systemSetting.Name == api.SystemSettingAdditionalScriptName {
|
} else if systemSetting.Name == api.SystemSettingAdditionalScriptName {
|
||||||
systemStatus.AdditionalScript = value.(string)
|
systemStatus.AdditionalScript = value.(string)
|
||||||
|
} else if systemSetting.Name == api.SystemSettingCustomizedProfileName {
|
||||||
|
valueMap := value.(map[string]interface{})
|
||||||
|
systemStatus.CustomizedProfile = api.CustomizedProfile{
|
||||||
|
Name: valueMap["name"].(string),
|
||||||
|
IconURL: valueMap["iconUrl"].(string),
|
||||||
|
ExternalURL: valueMap["externalUrl"].(string),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,6 +55,11 @@ const App = () => {
|
|||||||
scriptEl.innerHTML = systemStatus.additionalScript;
|
scriptEl.innerHTML = systemStatus.additionalScript;
|
||||||
document.head.appendChild(scriptEl);
|
document.head.appendChild(scriptEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dynamic update metadata with customized profile.
|
||||||
|
document.title = systemStatus.customizedProfile.name;
|
||||||
|
const link = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
|
||||||
|
link.href = systemStatus.customizedProfile.iconUrl || "/logo.webp";
|
||||||
}, [systemStatus]);
|
}, [systemStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { ANIMATION_DURATION } from "../../helpers/consts";
|
import { ANIMATION_DURATION } from "../../helpers/consts";
|
||||||
@ -10,8 +10,8 @@ import "../../less/base-dialog.less";
|
|||||||
|
|
||||||
interface DialogConfig {
|
interface DialogConfig {
|
||||||
className: string;
|
className: string;
|
||||||
clickSpaceDestroy?: boolean;
|
|
||||||
dialogName: string;
|
dialogName: string;
|
||||||
|
clickSpaceDestroy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends DialogConfig, DialogProps {
|
interface Props extends DialogConfig, DialogProps {
|
||||||
@ -21,13 +21,14 @@ interface Props extends DialogConfig, DialogProps {
|
|||||||
const BaseDialog: React.FC<Props> = (props: Props) => {
|
const BaseDialog: React.FC<Props> = (props: Props) => {
|
||||||
const { children, className, clickSpaceDestroy, dialogName, destroy } = props;
|
const { children, className, clickSpaceDestroy, dialogName, destroy } = props;
|
||||||
const dialogStore = useDialogStore();
|
const dialogStore = useDialogStore();
|
||||||
|
const dialogContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dialogIndex = dialogStore.state.dialogStack.findIndex((item) => item === dialogName);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dialogStore.pushDialogStack(dialogName);
|
dialogStore.pushDialogStack(dialogName);
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.code === "Escape") {
|
if (event.code === "Escape") {
|
||||||
if (dialogName === dialogStore.topDialogStack()) {
|
if (dialogName === dialogStore.topDialogStack()) {
|
||||||
dialogStore.popDialogStack();
|
|
||||||
destroy();
|
destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -37,9 +38,16 @@ const BaseDialog: React.FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.body.removeEventListener("keydown", handleKeyDown);
|
document.body.removeEventListener("keydown", handleKeyDown);
|
||||||
|
dialogStore.removeDialog(dialogName);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dialogIndex > 0 && dialogContainerRef.current) {
|
||||||
|
dialogContainerRef.current.style.marginTop = `${dialogIndex * 16}px`;
|
||||||
|
}
|
||||||
|
}, [dialogIndex]);
|
||||||
|
|
||||||
const handleSpaceClicked = () => {
|
const handleSpaceClicked = () => {
|
||||||
if (clickSpaceDestroy) {
|
if (clickSpaceDestroy) {
|
||||||
destroy();
|
destroy();
|
||||||
@ -48,7 +56,7 @@ const BaseDialog: React.FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`dialog-wrapper ${className}`} onMouseDown={handleSpaceClicked}>
|
<div className={`dialog-wrapper ${className}`} onMouseDown={handleSpaceClicked}>
|
||||||
<div className="dialog-container" onMouseDown={(e) => e.stopPropagation()}>
|
<div ref={dialogContainerRef} className="dialog-container" onMouseDown={(e) => e.stopPropagation()}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useUserStore } from "../store/module";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import { generateDialog } from "./Dialog";
|
import { generateDialog } from "./Dialog";
|
||||||
import MyAccountSection from "./Settings/MyAccountSection";
|
import MyAccountSection from "./Settings/MyAccountSection";
|
||||||
@ -7,7 +8,6 @@ import PreferencesSection from "./Settings/PreferencesSection";
|
|||||||
import MemberSection from "./Settings/MemberSection";
|
import MemberSection from "./Settings/MemberSection";
|
||||||
import SystemSection from "./Settings/SystemSection";
|
import SystemSection from "./Settings/SystemSection";
|
||||||
import "../less/setting-dialog.less";
|
import "../less/setting-dialog.less";
|
||||||
import { useUserStore } from "../store/module";
|
|
||||||
|
|
||||||
type Props = DialogProps;
|
type Props = DialogProps;
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
|
|||||||
onClick={() => handleSectionSelectorItemClick("system")}
|
onClick={() => handleSectionSelectorItemClick("system")}
|
||||||
className={`section-item ${state.selectedSection === "system" ? "selected" : ""}`}
|
className={`section-item ${state.selectedSection === "system" ? "selected" : ""}`}
|
||||||
>
|
>
|
||||||
<span className="icon-text">🧑🔧</span> {t("setting.system")}
|
<span className="icon-text">🛠️</span> {t("setting.system")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, Switch, Textarea } from "@mui/joy";
|
import { Button, Switch, Textarea } from "@mui/joy";
|
||||||
|
import { useGlobalStore } from "../../store/module";
|
||||||
import * as api from "../../helpers/api";
|
import * as api from "../../helpers/api";
|
||||||
import toastHelper from "../Toast";
|
import toastHelper from "../Toast";
|
||||||
import "../../less/settings/system-section.less";
|
import showUpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog";
|
||||||
|
import "@/less/settings/system-section.less";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
dbSize: number;
|
dbSize: number;
|
||||||
@ -23,25 +25,28 @@ const formatBytes = (bytes: number) => {
|
|||||||
|
|
||||||
const SystemSection = () => {
|
const SystemSection = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
const systemStatus = globalStore.state.systemStatus;
|
||||||
const [state, setState] = useState<State>({
|
const [state, setState] = useState<State>({
|
||||||
dbSize: 0,
|
dbSize: systemStatus.dbSize,
|
||||||
allowSignUp: false,
|
allowSignUp: systemStatus.allowSignUp,
|
||||||
additionalStyle: "",
|
additionalStyle: systemStatus.additionalStyle,
|
||||||
additionalScript: "",
|
additionalScript: systemStatus.additionalScript,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.getSystemStatus().then(({ data }) => {
|
globalStore.fetchSystemStatus();
|
||||||
const { data: status } = data;
|
|
||||||
setState({
|
|
||||||
dbSize: status.dbSize,
|
|
||||||
allowSignUp: status.allowSignUp,
|
|
||||||
additionalStyle: status.additionalStyle,
|
|
||||||
additionalScript: status.additionalScript,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setState({
|
||||||
|
dbSize: systemStatus.dbSize,
|
||||||
|
allowSignUp: systemStatus.allowSignUp,
|
||||||
|
additionalStyle: systemStatus.additionalStyle,
|
||||||
|
additionalScript: systemStatus.additionalScript,
|
||||||
|
});
|
||||||
|
}, [systemStatus]);
|
||||||
|
|
||||||
const handleAllowSignUpChanged = async (value: boolean) => {
|
const handleAllowSignUpChanged = async (value: boolean) => {
|
||||||
setState({
|
setState({
|
||||||
...state,
|
...state,
|
||||||
@ -60,16 +65,14 @@ const SystemSection = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdateCustomizedProfileButtonClick = () => {
|
||||||
|
showUpdateCustomizedProfileDialog();
|
||||||
|
};
|
||||||
|
|
||||||
const handleVacuumBtnClick = async () => {
|
const handleVacuumBtnClick = async () => {
|
||||||
try {
|
try {
|
||||||
await api.vacuumDatabase();
|
await api.vacuumDatabase();
|
||||||
const { data: status } = (await api.getSystemStatus()).data;
|
await globalStore.fetchSystemStatus();
|
||||||
setState({
|
|
||||||
dbSize: status.dbSize,
|
|
||||||
allowSignUp: status.allowSignUp,
|
|
||||||
additionalStyle: status.additionalStyle,
|
|
||||||
additionalScript: status.additionalScript,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return;
|
return;
|
||||||
@ -113,17 +116,23 @@ const SystemSection = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="section-container system-section-container">
|
<div className="section-container system-section-container">
|
||||||
<p className="title-text">{t("common.basic")}</p>
|
<p className="title-text">{t("common.basic")}</p>
|
||||||
<label className="form-label">
|
<div className="form-label">
|
||||||
|
<div className="normal-text">
|
||||||
|
Server name: <span className="font-mono font-bold">{systemStatus.customizedProfile.name}</span>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleUpdateCustomizedProfileButtonClick}>Edit</Button>
|
||||||
|
</div>
|
||||||
|
<div className="form-label">
|
||||||
<span className="normal-text">
|
<span className="normal-text">
|
||||||
{t("setting.system-section.database-file-size")}: <span className="font-mono font-medium">{formatBytes(state.dbSize)}</span>
|
{t("setting.system-section.database-file-size")}: <span className="font-mono font-bold">{formatBytes(state.dbSize)}</span>
|
||||||
</span>
|
</span>
|
||||||
<Button onClick={handleVacuumBtnClick}>{t("common.vacuum")}</Button>
|
<Button onClick={handleVacuumBtnClick}>{t("common.vacuum")}</Button>
|
||||||
</label>
|
</div>
|
||||||
<p className="title-text">{t("sidebar.setting")}</p>
|
<p className="title-text">{t("sidebar.setting")}</p>
|
||||||
<label className="form-label">
|
<div className="form-label">
|
||||||
<span className="normal-text">{t("setting.system-section.allow-user-signup")}</span>
|
<span className="normal-text">{t("setting.system-section.allow-user-signup")}</span>
|
||||||
<Switch checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} />
|
<Switch checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} />
|
||||||
</label>
|
</div>
|
||||||
<div className="form-label">
|
<div className="form-label">
|
||||||
<span className="normal-text">{t("setting.system-section.additional-style")}</span>
|
<span className="normal-text">{t("setting.system-section.additional-style")}</span>
|
||||||
<Button onClick={handleSaveAdditionalStyle}>{t("common.save")}</Button>
|
<Button onClick={handleSaveAdditionalStyle}>{t("common.save")}</Button>
|
||||||
|
100
web/src/components/UpdateCustomizedProfileDialog.tsx
Normal file
100
web/src/components/UpdateCustomizedProfileDialog.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useGlobalStore } from "../store/module";
|
||||||
|
import * as api from "../helpers/api";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
import { generateDialog } from "./Dialog";
|
||||||
|
import toastHelper from "./Toast";
|
||||||
|
|
||||||
|
type Props = DialogProps;
|
||||||
|
|
||||||
|
const UpdateCustomizedProfileDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
const [state, setState] = useState<CustomizedProfile>(globalStore.state.systemStatus.customizedProfile);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// do nth
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCloseBtnClick = () => {
|
||||||
|
destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setState((state) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
name: e.target.value as string,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIconUrlChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setState((state) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
iconUrl: e.target.value as string,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveBtnClick = async () => {
|
||||||
|
if (state.name === "" || state.iconUrl === "") {
|
||||||
|
toastHelper.error(t("message.fill-all"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.upsertSystemSetting({
|
||||||
|
name: "customizedProfile",
|
||||||
|
value: JSON.stringify(state),
|
||||||
|
});
|
||||||
|
await globalStore.fetchSystemStatus();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toastHelper.success("Succeed to update customized profile");
|
||||||
|
destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="dialog-header-container !w-64">
|
||||||
|
<p className="title-text">Customize server</p>
|
||||||
|
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||||
|
<Icon.X />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="dialog-content-container">
|
||||||
|
<p className="text-sm mb-1">
|
||||||
|
Server Name<span className="text-sm text-gray-400 ml-1">(Default is memos)</span>
|
||||||
|
</p>
|
||||||
|
<input type="text" className="input-text" value={state.name} onChange={handleNameChanged} />
|
||||||
|
<p className="text-sm mb-1 mt-2">Icon URL</p>
|
||||||
|
<input type="text" className="input-text" value={state.iconUrl} onChange={handleIconUrlChanged} />
|
||||||
|
<div className="mt-4 w-full flex flex-row justify-end items-center space-x-2">
|
||||||
|
<span className="btn-text" onClick={handleCloseBtnClick}>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</span>
|
||||||
|
<span className="btn-primary" onClick={handleSaveBtnClick}>
|
||||||
|
{t("common.save")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function showUpdateCustomizedProfileDialog() {
|
||||||
|
generateDialog(
|
||||||
|
{
|
||||||
|
className: "update-customized-profile-dialog",
|
||||||
|
dialogName: "update-customized-profile-dialog",
|
||||||
|
},
|
||||||
|
UpdateCustomizedProfileDialog
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default showUpdateCustomizedProfileDialog;
|
@ -14,11 +14,11 @@
|
|||||||
@apply w-full flex flex-row justify-start items-center;
|
@apply w-full flex flex-row justify-start items-center;
|
||||||
|
|
||||||
> .logo-img {
|
> .logo-img {
|
||||||
@apply h-20 w-auto;
|
@apply h-16 w-auto mr-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .logo-text {
|
> .logo-text {
|
||||||
@apply text-6xl tracking-wide text-black dark:text-gray-200;
|
@apply text-5xl tracking-wide text-black opacity-80 dark:text-gray-200;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
@apply flex flex-row justify-start items-center;
|
@apply flex flex-row justify-start items-center;
|
||||||
|
|
||||||
> .logo-img {
|
> .logo-img {
|
||||||
@apply h-12 sm:h-14 w-auto mr-1;
|
@apply h-12 sm:h-14 w-auto mr-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .title-text {
|
> .title-text {
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
@apply flex flex-row justify-start items-center;
|
@apply flex flex-row justify-start items-center;
|
||||||
|
|
||||||
> .logo-img {
|
> .logo-img {
|
||||||
@apply h-12 sm:h-14 w-auto mr-1;
|
@apply h-12 sm:h-14 w-auto mr-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .logo-text {
|
> .logo-text {
|
||||||
|
@ -23,8 +23,8 @@ const Auth = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const globalStore = useGlobalStore();
|
const globalStore = useGlobalStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const systemStatus = globalStore.state.systemStatus;
|
|
||||||
const actionBtnLoadingState = useLoading(false);
|
const actionBtnLoadingState = useLoading(false);
|
||||||
|
const systemStatus = globalStore.state.systemStatus;
|
||||||
const mode = systemStatus.profile.mode;
|
const mode = systemStatus.profile.mode;
|
||||||
const [username, setUsername] = useState(mode === "dev" ? "demohero" : "");
|
const [username, setUsername] = useState(mode === "dev" ? "demohero" : "");
|
||||||
const [password, setPassword] = useState(mode === "dev" ? "secret" : "");
|
const [password, setPassword] = useState(mode === "dev" ? "secret" : "");
|
||||||
@ -119,8 +119,8 @@ const Auth = () => {
|
|||||||
<div className="auth-form-wrapper">
|
<div className="auth-form-wrapper">
|
||||||
<div className="page-header-container">
|
<div className="page-header-container">
|
||||||
<div className="title-container">
|
<div className="title-container">
|
||||||
<img className="logo-img" src="/logo.webp" alt="" />
|
<img className="logo-img" src={systemStatus.customizedProfile.iconUrl} alt="" />
|
||||||
<p className="logo-text">memos</p>
|
<p className="logo-text">{systemStatus.customizedProfile.name}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="slogan-text">{t("slogan")}</p>
|
<p className="slogan-text">{t("slogan")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,7 +2,7 @@ import dayjs from "dayjs";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useLocationStore, useMemoStore, useUserStore } from "../store/module";
|
import { useGlobalStore, useLocationStore, useMemoStore, useUserStore } from "../store/module";
|
||||||
import { DEFAULT_MEMO_LIMIT } from "../helpers/consts";
|
import { DEFAULT_MEMO_LIMIT } from "../helpers/consts";
|
||||||
import useLoading from "../hooks/useLoading";
|
import useLoading from "../hooks/useLoading";
|
||||||
import toastHelper from "../components/Toast";
|
import toastHelper from "../components/Toast";
|
||||||
@ -16,16 +16,18 @@ interface State {
|
|||||||
|
|
||||||
const Explore = () => {
|
const Explore = () => {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
const locationStore = useLocationStore();
|
const locationStore = useLocationStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const memoStore = useMemoStore();
|
const memoStore = useMemoStore();
|
||||||
const user = userStore.state.user;
|
|
||||||
const location = locationStore.state;
|
|
||||||
const [state, setState] = useState<State>({
|
const [state, setState] = useState<State>({
|
||||||
memos: [],
|
memos: [],
|
||||||
});
|
});
|
||||||
const [isComplete, setIsComplete] = useState<boolean>(false);
|
const [isComplete, setIsComplete] = useState<boolean>(false);
|
||||||
const loadingState = useLoading();
|
const loadingState = useLoading();
|
||||||
|
const customizedProfile = globalStore.state.systemStatus.customizedProfile;
|
||||||
|
const user = userStore.state.user;
|
||||||
|
const location = locationStore.state;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, state.memos.length).then((memos) => {
|
memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, state.memos.length).then((memos) => {
|
||||||
@ -61,8 +63,8 @@ const Explore = () => {
|
|||||||
<div className="page-container">
|
<div className="page-container">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="title-container">
|
<div className="title-container">
|
||||||
<img className="logo-img" src="/logo.webp" alt="" />
|
<img className="logo-img" src={customizedProfile.iconUrl} alt="" />
|
||||||
<span className="title-text">memos</span>
|
<span className="title-text">{customizedProfile.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="action-button-container">
|
<div className="action-button-container">
|
||||||
{!loadingState.isLoading && user ? (
|
{!loadingState.isLoading && user ? (
|
||||||
|
@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useParams } from "react-router-dom";
|
||||||
import { UNKNOWN_ID } from "../helpers/consts";
|
import { UNKNOWN_ID } from "../helpers/consts";
|
||||||
import { useLocationStore, useMemoStore, useUserStore } from "../store/module";
|
import { useGlobalStore, useLocationStore, useMemoStore, useUserStore } from "../store/module";
|
||||||
import useLoading from "../hooks/useLoading";
|
import useLoading from "../hooks/useLoading";
|
||||||
import toastHelper from "../components/Toast";
|
import toastHelper from "../components/Toast";
|
||||||
import MemoContent from "../components/MemoContent";
|
import MemoContent from "../components/MemoContent";
|
||||||
@ -17,17 +17,19 @@ interface State {
|
|||||||
const MemoDetail = () => {
|
const MemoDetail = () => {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
const locationStore = useLocationStore();
|
const locationStore = useLocationStore();
|
||||||
const memoStore = useMemoStore();
|
const memoStore = useMemoStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const user = userStore.state.user;
|
|
||||||
const location = locationStore.state;
|
|
||||||
const [state, setState] = useState<State>({
|
const [state, setState] = useState<State>({
|
||||||
memo: {
|
memo: {
|
||||||
id: UNKNOWN_ID,
|
id: UNKNOWN_ID,
|
||||||
} as Memo,
|
} as Memo,
|
||||||
});
|
});
|
||||||
const loadingState = useLoading();
|
const loadingState = useLoading();
|
||||||
|
const customizedProfile = globalStore.state.systemStatus.customizedProfile;
|
||||||
|
const user = userStore.state.user;
|
||||||
|
const location = locationStore.state;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const memoId = Number(params.memoId);
|
const memoId = Number(params.memoId);
|
||||||
@ -52,8 +54,8 @@ const MemoDetail = () => {
|
|||||||
<div className="page-container">
|
<div className="page-container">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="title-container">
|
<div className="title-container">
|
||||||
<img className="logo-img" src="/logo.webp" alt="" />
|
<img className="logo-img" src={customizedProfile.iconUrl} alt="" />
|
||||||
<p className="logo-text">memos</p>
|
<p className="logo-text">{customizedProfile.name}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="action-button-container">
|
<div className="action-button-container">
|
||||||
{!loadingState.isLoading && (
|
{!loadingState.isLoading && (
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import store, { useAppSelector } from "..";
|
|
||||||
import { pushDialogStack, popDialogStack } from "../reducer/dialog";
|
|
||||||
import { last } from "lodash";
|
import { last } from "lodash";
|
||||||
|
import store, { useAppSelector } from "..";
|
||||||
|
import { pushDialogStack, popDialogStack, removeDialog } from "../reducer/dialog";
|
||||||
|
|
||||||
export const useDialogStore = () => {
|
export const useDialogStore = () => {
|
||||||
const state = useAppSelector((state) => state.editor);
|
const state = useAppSelector((state) => state.dialog);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
@ -16,6 +16,9 @@ export const useDialogStore = () => {
|
|||||||
popDialogStack: () => {
|
popDialogStack: () => {
|
||||||
store.dispatch(popDialogStack());
|
store.dispatch(popDialogStack());
|
||||||
},
|
},
|
||||||
|
removeDialog: (dialogName: string) => {
|
||||||
|
store.dispatch(removeDialog(dialogName));
|
||||||
|
},
|
||||||
topDialogStack: () => {
|
topDialogStack: () => {
|
||||||
return last(store.getState().dialog.dialogStack);
|
return last(store.getState().dialog.dialogStack);
|
||||||
},
|
},
|
||||||
|
@ -11,6 +11,11 @@ export const initialGlobalState = async () => {
|
|||||||
allowSignUp: false,
|
allowSignUp: false,
|
||||||
additionalStyle: "",
|
additionalStyle: "",
|
||||||
additionalScript: "",
|
additionalScript: "",
|
||||||
|
customizedProfile: {
|
||||||
|
name: "memos",
|
||||||
|
iconUrl: "/logo.webp",
|
||||||
|
externalUrl: "",
|
||||||
|
},
|
||||||
} as SystemStatus,
|
} as SystemStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -42,6 +47,11 @@ export const useGlobalStore = () => {
|
|||||||
getState: () => {
|
getState: () => {
|
||||||
return store.getState().global;
|
return store.getState().global;
|
||||||
},
|
},
|
||||||
|
fetchSystemStatus: async () => {
|
||||||
|
const { data: systemStatus } = (await api.getSystemStatus()).data;
|
||||||
|
store.dispatch(setGlobalState({ systemStatus: systemStatus }));
|
||||||
|
return systemStatus;
|
||||||
|
},
|
||||||
setLocale: (locale: Locale) => {
|
setLocale: (locale: Locale) => {
|
||||||
store.dispatch(setLocale(locale));
|
store.dispatch(setLocale(locale));
|
||||||
},
|
},
|
||||||
|
@ -22,9 +22,16 @@ const dialogSlice = createSlice({
|
|||||||
dialogStack: state.dialogStack.slice(0, state.dialogStack.length - 1),
|
dialogStack: state.dialogStack.slice(0, state.dialogStack.length - 1),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
removeDialog: (state, action: PayloadAction<string>) => {
|
||||||
|
const filterDialogStack = state.dialogStack.filter((dialogName) => dialogName !== action.payload);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
dialogStack: filterDialogStack,
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { pushDialogStack, popDialogStack } = dialogSlice.actions;
|
export const { pushDialogStack, popDialogStack, removeDialog } = dialogSlice.actions;
|
||||||
|
|
||||||
export default dialogSlice.reducer;
|
export default dialogSlice.reducer;
|
||||||
|
@ -21,11 +21,19 @@ const globalSlice = createSlice({
|
|||||||
allowSignUp: false,
|
allowSignUp: false,
|
||||||
additionalStyle: "",
|
additionalStyle: "",
|
||||||
additionalScript: "",
|
additionalScript: "",
|
||||||
|
customizedProfile: {
|
||||||
|
name: "memos",
|
||||||
|
iconUrl: "/logo.webp",
|
||||||
|
externalUrl: "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as State,
|
} as State,
|
||||||
reducers: {
|
reducers: {
|
||||||
setGlobalState: (_, action: PayloadAction<State>) => {
|
setGlobalState: (state, action: PayloadAction<Partial<State>>) => {
|
||||||
return action.payload;
|
return {
|
||||||
|
...state,
|
||||||
|
...action.payload,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
setLocale: (state, action: PayloadAction<Locale>) => {
|
setLocale: (state, action: PayloadAction<Locale>) => {
|
||||||
return {
|
return {
|
||||||
|
7
web/src/types/modules/system.d.ts
vendored
7
web/src/types/modules/system.d.ts
vendored
@ -3,6 +3,12 @@ interface Profile {
|
|||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CustomizedProfile {
|
||||||
|
name: string;
|
||||||
|
iconUrl: string;
|
||||||
|
externalUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface SystemStatus {
|
interface SystemStatus {
|
||||||
host?: User;
|
host?: User;
|
||||||
profile: Profile;
|
profile: Profile;
|
||||||
@ -11,6 +17,7 @@ interface SystemStatus {
|
|||||||
allowSignUp: boolean;
|
allowSignUp: boolean;
|
||||||
additionalStyle: string;
|
additionalStyle: string;
|
||||||
additionalScript: string;
|
additionalScript: string;
|
||||||
|
customizedProfile: CustomizedProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SystemSetting {
|
interface SystemSetting {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { resolve } from "path";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import legacy from "@vitejs/plugin-legacy";
|
import legacy from "@vitejs/plugin-legacy";
|
||||||
import react from "@vitejs/plugin-react-swc";
|
import react from "@vitejs/plugin-react-swc";
|
||||||
@ -32,4 +33,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@/": `${resolve(__dirname, "src")}/`,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user