mirror of
https://github.com/aykhans/slash-e.git
synced 2025-04-18 13:15:27 +00:00
feat: implement custom branding
This commit is contained in:
parent
a113d82e9b
commit
b6271938b3
@ -8,6 +8,7 @@ import { PlanType } from "@/types/proto/api/v1/subscription_service";
|
||||
import { Role } from "@/types/proto/api/v1/user_service";
|
||||
import AboutDialog from "./AboutDialog";
|
||||
import Icon from "./Icon";
|
||||
import Logo from "./Logo";
|
||||
import Dropdown from "./common/Dropdown";
|
||||
|
||||
const Header: React.FC = () => {
|
||||
@ -32,7 +33,7 @@ const Header: React.FC = () => {
|
||||
<div className="w-full max-w-8xl mx-auto px-4 sm:px-6 md:px-12 py-3 flex flex-row justify-between items-center">
|
||||
<div className="flex flex-row justify-start items-center shrink mr-2">
|
||||
<Link to="/" className="cursor-pointer flex flex-row justify-start items-center dark:text-gray-400" unstable_viewTransition>
|
||||
<Icon.CircleSlash className="w-7 h-auto dark:text-gray-500 mr-2" strokeWidth={1.5} />
|
||||
<Logo className="mr-2" />
|
||||
Slash
|
||||
</Link>
|
||||
{profile.plan === PlanType.PRO && (
|
||||
|
23
frontend/web/src/components/Logo.tsx
Normal file
23
frontend/web/src/components/Logo.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import classNames from "classnames";
|
||||
import { useWorkspaceStore } from "@/stores";
|
||||
import Icon from "./Icon";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Logo = ({ className }: Props) => {
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const branding = workspaceStore.setting.branding ? new TextDecoder().decode(workspaceStore.setting.branding) : "";
|
||||
return (
|
||||
<div className={classNames("w-8 h-auto dark:text-gray-500 rounded-lg overflow-hidden", className)}>
|
||||
{branding ? (
|
||||
<img src={branding} alt="branding" className="max-w-full max-h-full" />
|
||||
) : (
|
||||
<Icon.CircleSlash className="w-full h-auto" strokeWidth={1.5} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logo;
|
@ -1,5 +1,5 @@
|
||||
import { Button, Option, Select, Textarea } from "@mui/joy";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { head, isEqual } from "lodash-es";
|
||||
import { useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@ -7,6 +7,7 @@ import { workspaceServiceClient } from "@/grpcweb";
|
||||
import { useWorkspaceStore } from "@/stores";
|
||||
import { Visibility } from "@/types/proto/api/v1/common";
|
||||
import { WorkspaceSetting } from "@/types/proto/api/v1/workspace_service";
|
||||
import Icon from "../Icon";
|
||||
|
||||
const getDefaultVisibility = (visibility?: Visibility) => {
|
||||
if (!visibility || [Visibility.VISIBILITY_UNSPECIFIED, Visibility.UNRECOGNIZED].includes(visibility)) {
|
||||
@ -16,12 +17,32 @@ const getDefaultVisibility = (visibility?: Visibility) => {
|
||||
return visibility;
|
||||
};
|
||||
|
||||
const convertFileToBase64 = (file: File) =>
|
||||
new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
|
||||
const WorkspaceSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const [workspaceSetting, setWorkspaceSetting] = useState<WorkspaceSetting>(workspaceStore.setting);
|
||||
const originalWorkspaceSetting = useRef<WorkspaceSetting>(workspaceStore.setting);
|
||||
const allowSave = !isEqual(originalWorkspaceSetting.current, workspaceSetting);
|
||||
const branding = workspaceSetting.branding ? new TextDecoder().decode(workspaceSetting.branding) : "";
|
||||
|
||||
const onBrandingChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files: File[] = Array.from(event.target.files || []);
|
||||
const file = head(files);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const base64 = await convertFileToBase64(file);
|
||||
setWorkspaceSetting({ ...workspaceSetting, branding: new TextEncoder().encode(base64) });
|
||||
};
|
||||
|
||||
const handleCustomStyleChange = async (value: string) => {
|
||||
setWorkspaceSetting({
|
||||
@ -39,6 +60,9 @@ const WorkspaceSection = () => {
|
||||
|
||||
const handleSaveWorkspaceSetting = async () => {
|
||||
const updateMask: string[] = [];
|
||||
if (!isEqual(originalWorkspaceSetting.current.branding, workspaceSetting.branding)) {
|
||||
updateMask.push("branding");
|
||||
}
|
||||
if (!isEqual(originalWorkspaceSetting.current.customStyle, workspaceSetting.customStyle)) {
|
||||
updateMask.push("custom_style");
|
||||
}
|
||||
@ -70,6 +94,26 @@ const WorkspaceSection = () => {
|
||||
<div className="w-full flex flex-col sm:flex-row justify-start items-start gap-4 sm:gap-x-16">
|
||||
<p className="sm:w-1/4 text-2xl shrink-0 font-semibold text-gray-900 dark:text-gray-500">{t("settings.workspace.self")}</p>
|
||||
<div className="w-full sm:w-auto grow flex flex-col justify-start items-start gap-4">
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<div className="w-full flex flex-col justify-start items-start">
|
||||
<p className="font-medium dark:text-gray-400">Custom branding</p>
|
||||
<p className="text-sm text-gray-500 leading-tight">Recommand logo ratio: 1:1</p>
|
||||
</div>
|
||||
<div className="relative shrink-0 hover:opacity-80">
|
||||
{branding ? (
|
||||
<div className="relative w-16 h-16">
|
||||
<img src={branding} alt="branding" className="max-w-full max-h-full" />
|
||||
<Icon.X
|
||||
className="w-4 h-auto -top-2 -right-2 absolute z-1 border rounded-full bg-white opacity-80"
|
||||
onClick={() => setWorkspaceSetting({ ...workspaceSetting, branding: new TextEncoder().encode("") })}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Icon.CircleSlash className="w-7 h-auto dark:text-gray-500 mr-2" strokeWidth={1.5} />
|
||||
)}
|
||||
<input className="absolute inset-0 z-1 opacity-0" type="file" accept=".jpg,.jpeg,.png,.svg,.webp" onChange={onBrandingChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<div className="w-full flex flex-col justify-start items-start">
|
||||
<p className="font-medium dark:text-gray-400">{t("settings.workspace.default-visibility")}</p>
|
||||
|
@ -3,7 +3,7 @@ import React, { FormEvent, useEffect, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import Icon from "@/components/Icon";
|
||||
import Logo from "@/components/Logo";
|
||||
import { authServiceClient } from "@/grpcweb";
|
||||
import useLoading from "@/hooks/useLoading";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
@ -64,7 +64,7 @@ const SignIn: React.FC = () => {
|
||||
<div className="w-80 max-w-full h-full py-4 flex flex-col justify-start items-center">
|
||||
<div className="w-full py-4 grow flex flex-col justify-center items-center">
|
||||
<div className="flex flex-row justify-start items-center w-auto mx-auto gap-y-2 mb-4">
|
||||
<Icon.CircleSlash className="w-10 h-auto dark:text-gray-500 mr-2" strokeWidth={1.5} />
|
||||
<Logo className="mr-2" />
|
||||
<span className="text-3xl opacity-80 dark:text-gray-500">Slash</span>
|
||||
</div>
|
||||
<form className="w-full mt-6" onSubmit={handleSigninBtnClick}>
|
||||
|
@ -3,7 +3,7 @@ import React, { FormEvent, useEffect, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import Icon from "@/components/Icon";
|
||||
import Logo from "@/components/Logo";
|
||||
import { authServiceClient } from "@/grpcweb";
|
||||
import useLoading from "@/hooks/useLoading";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
@ -75,7 +75,7 @@ const SignUp: React.FC = () => {
|
||||
<div className="w-80 max-w-full h-full py-4 flex flex-col justify-start items-center">
|
||||
<div className="w-full py-4 grow flex flex-col justify-center items-center">
|
||||
<div className="flex flex-row justify-start items-center w-auto mx-auto gap-y-2 mb-4">
|
||||
<Icon.CircleSlash className="w-10 h-auto dark:text-gray-500 mr-2" strokeWidth={1.5} />
|
||||
<Logo className="mr-2" />
|
||||
<span className="text-3xl opacity-80 dark:text-gray-500">Slash</span>
|
||||
</div>
|
||||
<p className="w-full text-2xl mt-6 dark:text-gray-500">{t("auth.create-your-account")}</p>
|
||||
|
@ -61,6 +61,7 @@ func (s *APIV1Service) GetWorkspaceSetting(ctx context.Context, _ *v1pb.GetWorks
|
||||
for _, v := range workspaceSettings {
|
||||
if v.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_GENERAL {
|
||||
generalSetting := v.GetGeneral()
|
||||
workspaceSetting.Branding = generalSetting.GetBranding()
|
||||
workspaceSetting.CustomStyle = generalSetting.GetCustomStyle()
|
||||
} else if v.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_SHORTCUT_RELATED {
|
||||
shortcutRelatedSetting := v.GetShortcutRelated()
|
||||
|
Loading…
x
Reference in New Issue
Block a user