mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Chore: mobx setup and app sidebar and theme management (#1798)
* dev: Mobx integration for app sidebar and custom theme * dev: Handled edge case and conditional rendering for mobx store
This commit is contained in:
parent
a164dfd532
commit
b6744dcd29
@ -24,8 +24,12 @@ import issuesService from "services/issues.service";
|
|||||||
import inboxService from "services/inbox.service";
|
import inboxService from "services/inbox.service";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { INBOX_LIST, ISSUE_DETAILS } from "constants/fetch-keys";
|
import { INBOX_LIST, ISSUE_DETAILS } from "constants/fetch-keys";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
export const CommandPalette: React.FC = () => {
|
export const CommandPalette: React.FC = () => {
|
||||||
|
const store: any = useMobxStore();
|
||||||
|
|
||||||
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
|
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
|
||||||
const [isIssueModalOpen, setIsIssueModalOpen] = useState(false);
|
const [isIssueModalOpen, setIsIssueModalOpen] = useState(false);
|
||||||
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
|
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
|
||||||
@ -96,7 +100,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
setIsIssueModalOpen(true);
|
setIsIssueModalOpen(true);
|
||||||
} else if ((ctrlKey || metaKey) && keyPressed === "b") {
|
} else if ((ctrlKey || metaKey) && keyPressed === "b") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
toggleCollapsed();
|
// toggleCollapsed();
|
||||||
|
store.theme.setSidebarCollapsed(!store?.theme?.sidebarCollapsed);
|
||||||
} else if (key === "Delete") {
|
} else if (key === "Delete") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsBulkDeleteIssuesModalOpen(true);
|
setIsBulkDeleteIssuesModalOpen(true);
|
||||||
@ -120,7 +125,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[toggleCollapsed, copyIssueUrlToClipboard]
|
[copyIssueUrlToClipboard]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -15,6 +15,8 @@ import userService from "services/user.service";
|
|||||||
import { applyTheme } from "helpers/theme.helper";
|
import { applyTheme } from "helpers/theme.helper";
|
||||||
// types
|
// types
|
||||||
import { ICustomTheme } from "types";
|
import { ICustomTheme } from "types";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
preLoadedData?: Partial<ICustomTheme> | null;
|
preLoadedData?: Partial<ICustomTheme> | null;
|
||||||
@ -32,6 +34,8 @@ const defaultValues: ICustomTheme = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
|
export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
|
||||||
|
const store: any = useMobxStore();
|
||||||
|
|
||||||
const [darkPalette, setDarkPalette] = useState(false);
|
const [darkPalette, setDarkPalette] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -60,21 +64,15 @@ export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
|
|||||||
theme: "custom",
|
theme: "custom",
|
||||||
};
|
};
|
||||||
|
|
||||||
await userService
|
store.user
|
||||||
.updateUser({
|
.updateCurrentUserSettings({ theme: payload })
|
||||||
theme: payload,
|
.then((response: any) => {
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
mutateUser((prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
|
|
||||||
return { ...prevData, ...res };
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
setTheme("custom");
|
setTheme("custom");
|
||||||
applyTheme(payload.palette, darkPalette);
|
applyTheme(payload.palette, darkPalette);
|
||||||
})
|
})
|
||||||
.catch((err) => console.log(err));
|
.catch((error: any) => {
|
||||||
|
console.log("error", error);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateTheme = async (formData: any) => {
|
const handleUpdateTheme = async (formData: any) => {
|
||||||
|
@ -21,8 +21,12 @@ import { NotificationsOutlined } from "@mui/icons-material";
|
|||||||
import emptyNotification from "public/empty-state/notification.svg";
|
import emptyNotification from "public/empty-state/notification.svg";
|
||||||
// helpers
|
// helpers
|
||||||
import { getNumberCount } from "helpers/string.helper";
|
import { getNumberCount } from "helpers/string.helper";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
export const NotificationPopover = () => {
|
export const NotificationPopover = () => {
|
||||||
|
const store: any = useMobxStore();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
notifications,
|
notifications,
|
||||||
archived,
|
archived,
|
||||||
@ -77,17 +81,17 @@ export const NotificationPopover = () => {
|
|||||||
tooltipContent="Notifications"
|
tooltipContent="Notifications"
|
||||||
position="right"
|
position="right"
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
disabled={!sidebarCollapse}
|
disabled={!store?.theme?.sidebarCollapsed}
|
||||||
>
|
>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
||||||
isActive
|
isActive
|
||||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||||
} ${sidebarCollapse ? "justify-center" : ""}`}
|
} ${store?.theme?.sidebarCollapsed ? "justify-center" : ""}`}
|
||||||
>
|
>
|
||||||
<NotificationsOutlined fontSize="small" />
|
<NotificationsOutlined fontSize="small" />
|
||||||
{sidebarCollapse ? null : <span>Notifications</span>}
|
{store?.theme?.sidebarCollapsed ? null : <span>Notifications</span>}
|
||||||
{totalNotificationCount && totalNotificationCount > 0 ? (
|
{totalNotificationCount && totalNotificationCount > 0 ? (
|
||||||
<span className="ml-auto bg-custom-primary-300 rounded-full text-xs text-white px-1.5">
|
<span className="ml-auto bg-custom-primary-300 rounded-full text-xs text-white px-1.5">
|
||||||
{getNumberCount(totalNotificationCount)}
|
{getNumberCount(totalNotificationCount)}
|
||||||
|
@ -26,8 +26,12 @@ import { orderArrayBy } from "helpers/array.helper";
|
|||||||
import { IProject } from "types";
|
import { IProject } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECTS_LIST } from "constants/fetch-keys";
|
import { PROJECTS_LIST } from "constants/fetch-keys";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
export const ProjectSidebarList: FC = () => {
|
export const ProjectSidebarList: FC = () => {
|
||||||
|
const store: any = useMobxStore();
|
||||||
|
|
||||||
const [deleteProjectModal, setDeleteProjectModal] = useState(false);
|
const [deleteProjectModal, setDeleteProjectModal] = useState(false);
|
||||||
const [projectToDelete, setProjectToDelete] = useState<IProject | null>(null);
|
const [projectToDelete, setProjectToDelete] = useState<IProject | null>(null);
|
||||||
|
|
||||||
@ -139,7 +143,7 @@ export const ProjectSidebarList: FC = () => {
|
|||||||
<Disclosure as="div" className="flex flex-col space-y-2" defaultOpen={true}>
|
<Disclosure as="div" className="flex flex-col space-y-2" defaultOpen={true}>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
{!sidebarCollapse && (
|
{!store?.theme?.sidebarCollapsed && (
|
||||||
<Disclosure.Button
|
<Disclosure.Button
|
||||||
as="button"
|
as="button"
|
||||||
type="button"
|
type="button"
|
||||||
@ -165,7 +169,7 @@ export const ProjectSidebarList: FC = () => {
|
|||||||
<SingleSidebarProject
|
<SingleSidebarProject
|
||||||
key={project.id}
|
key={project.id}
|
||||||
project={project}
|
project={project}
|
||||||
sidebarCollapse={sidebarCollapse}
|
sidebarCollapse={store?.theme?.sidebarCollapsed}
|
||||||
provided={provided}
|
provided={provided}
|
||||||
snapshot={snapshot}
|
snapshot={snapshot}
|
||||||
handleDeleteProject={() => handleDeleteProject(project)}
|
handleDeleteProject={() => handleDeleteProject(project)}
|
||||||
@ -194,7 +198,7 @@ export const ProjectSidebarList: FC = () => {
|
|||||||
<Disclosure as="div" className="flex flex-col space-y-2" defaultOpen={true}>
|
<Disclosure as="div" className="flex flex-col space-y-2" defaultOpen={true}>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
{!sidebarCollapse && (
|
{!store?.theme?.sidebarCollapsed && (
|
||||||
<Disclosure.Button
|
<Disclosure.Button
|
||||||
as="button"
|
as="button"
|
||||||
type="button"
|
type="button"
|
||||||
@ -215,7 +219,7 @@ export const ProjectSidebarList: FC = () => {
|
|||||||
<SingleSidebarProject
|
<SingleSidebarProject
|
||||||
key={project.id}
|
key={project.id}
|
||||||
project={project}
|
project={project}
|
||||||
sidebarCollapse={sidebarCollapse}
|
sidebarCollapse={store?.theme?.sidebarCollapsed}
|
||||||
provided={provided}
|
provided={provided}
|
||||||
snapshot={snapshot}
|
snapshot={snapshot}
|
||||||
handleDeleteProject={() => handleDeleteProject(project)}
|
handleDeleteProject={() => handleDeleteProject(project)}
|
||||||
@ -243,7 +247,7 @@ export const ProjectSidebarList: FC = () => {
|
|||||||
>
|
>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
{!sidebarCollapse && (
|
{!store?.theme?.sidebarCollapsed && (
|
||||||
<Disclosure.Button
|
<Disclosure.Button
|
||||||
as="button"
|
as="button"
|
||||||
type="button"
|
type="button"
|
||||||
@ -261,7 +265,7 @@ export const ProjectSidebarList: FC = () => {
|
|||||||
<SingleSidebarProject
|
<SingleSidebarProject
|
||||||
key={project.id}
|
key={project.id}
|
||||||
project={project}
|
project={project}
|
||||||
sidebarCollapse={sidebarCollapse}
|
sidebarCollapse={store?.theme?.sidebarCollapsed}
|
||||||
handleDeleteProject={() => handleDeleteProject(project)}
|
handleDeleteProject={() => handleDeleteProject(project)}
|
||||||
handleCopyText={() => handleCopyText(project.id)}
|
handleCopyText={() => handleCopyText(project.id)}
|
||||||
shortContextMenu
|
shortContextMenu
|
||||||
@ -284,7 +288,7 @@ export const ProjectSidebarList: FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-5 w-5" />
|
<PlusIcon className="h-5 w-5" />
|
||||||
{!sidebarCollapse && "Add Project"}
|
{!store?.theme?.sidebarCollapsed && "Add Project"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,6 +11,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
|||||||
import { Bolt, HelpOutlineOutlined, WestOutlined } from "@mui/icons-material";
|
import { Bolt, HelpOutlineOutlined, WestOutlined } from "@mui/icons-material";
|
||||||
import { ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline";
|
import { ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline";
|
||||||
import { DocumentIcon, DiscordIcon, GithubIcon } from "components/icons";
|
import { DocumentIcon, DiscordIcon, GithubIcon } from "components/icons";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
const helpOptions = [
|
const helpOptions = [
|
||||||
{
|
{
|
||||||
@ -41,6 +43,8 @@ export interface WorkspaceHelpSectionProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setSidebarActive }) => {
|
export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setSidebarActive }) => {
|
||||||
|
const store: any = useMobxStore();
|
||||||
|
|
||||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||||
|
|
||||||
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
|
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
|
||||||
@ -53,23 +57,23 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setS
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 py-2 px-4 ${
|
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 py-2 px-4 ${
|
||||||
sidebarCollapse ? "flex-col" : ""
|
store?.theme?.sidebarCollapsed ? "flex-col" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{!sidebarCollapse && (
|
{!store?.theme?.sidebarCollapsed && (
|
||||||
<div className="w-1/2 text-center cursor-default rounded-md px-2.5 py-1.5 font-medium outline-none text-sm bg-green-500/10 text-green-500">
|
<div className="w-1/2 text-center cursor-default rounded-md px-2.5 py-1.5 font-medium outline-none text-sm bg-green-500/10 text-green-500">
|
||||||
Free Plan
|
Free Plan
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-1 ${
|
className={`flex items-center gap-1 ${
|
||||||
sidebarCollapse ? "flex-col justify-center" : "justify-evenly w-1/2"
|
store?.theme?.sidebarCollapsed ? "flex-col justify-center" : "justify-evenly w-1/2"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
|
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
|
||||||
sidebarCollapse ? "w-full" : ""
|
store?.theme?.sidebarCollapsed ? "w-full" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const e = new KeyboardEvent("keydown", {
|
const e = new KeyboardEvent("keydown", {
|
||||||
@ -83,7 +87,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setS
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
|
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
|
||||||
sidebarCollapse ? "w-full" : ""
|
store?.theme?.sidebarCollapsed ? "w-full" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
||||||
>
|
>
|
||||||
@ -99,13 +103,13 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setS
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`hidden md:grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
|
className={`hidden md:grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
|
||||||
sidebarCollapse ? "w-full" : ""
|
store?.theme?.sidebarCollapsed ? "w-full" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => toggleCollapsed()}
|
onClick={() => store.theme.setSidebarCollapsed(!store?.theme?.sidebarCollapsed)}
|
||||||
>
|
>
|
||||||
<WestOutlined
|
<WestOutlined
|
||||||
fontSize="small"
|
fontSize="small"
|
||||||
className={`duration-300 ${sidebarCollapse ? "rotate-180" : ""}`}
|
className={`duration-300 ${store?.theme?.sidebarCollapsed ? "rotate-180" : ""}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -122,7 +126,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setS
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`absolute bottom-2 ${
|
className={`absolute bottom-2 ${
|
||||||
sidebarCollapse ? "left-full" : "left-[-75px]"
|
store?.theme?.sidebarCollapsed ? "left-full" : "left-[-75px]"
|
||||||
} space-y-2 rounded-sm bg-custom-background-80 p-1 shadow-md`}
|
} space-y-2 rounded-sm bg-custom-background-80 p-1 shadow-md`}
|
||||||
ref={helpOptionsRef}
|
ref={helpOptionsRef}
|
||||||
>
|
>
|
||||||
|
@ -23,6 +23,8 @@ import { CheckIcon, PlusIcon } from "@heroicons/react/24/outline";
|
|||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IWorkspace } from "types";
|
import { IWorkspace } from "types";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
// Static Data
|
// Static Data
|
||||||
const userLinks = (workspaceSlug: string, userId: string) => [
|
const userLinks = (workspaceSlug: string, userId: string) => [
|
||||||
@ -54,6 +56,8 @@ const profileLinks = (workspaceSlug: string, userId: string) => [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const WorkspaceSidebarDropdown = () => {
|
export const WorkspaceSidebarDropdown = () => {
|
||||||
|
const store: any = useMobxStore();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
@ -108,7 +112,7 @@ export const WorkspaceSidebarDropdown = () => {
|
|||||||
<Menu.Button className="text-custom-sidebar-text-200 flex w-full items-center rounded-sm text-sm font-medium focus:outline-none">
|
<Menu.Button className="text-custom-sidebar-text-200 flex w-full items-center rounded-sm text-sm font-medium focus:outline-none">
|
||||||
<div
|
<div
|
||||||
className={`flex w-full items-center gap-x-2 rounded-sm bg-custom-sidebar-background-80 p-1 ${
|
className={`flex w-full items-center gap-x-2 rounded-sm bg-custom-sidebar-background-80 p-1 ${
|
||||||
sidebarCollapse ? "justify-center" : ""
|
store?.theme?.sidebarCollapsed ? "justify-center" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="relative grid h-6 w-6 place-items-center rounded bg-gray-700 uppercase text-white">
|
<div className="relative grid h-6 w-6 place-items-center rounded bg-gray-700 uppercase text-white">
|
||||||
@ -123,7 +127,7 @@ export const WorkspaceSidebarDropdown = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!sidebarCollapse && (
|
{!store?.theme?.sidebarCollapsed && (
|
||||||
<h4 className="text-custom-text-100">
|
<h4 className="text-custom-text-100">
|
||||||
{activeWorkspace?.name ? truncateText(activeWorkspace.name, 14) : "Loading..."}
|
{activeWorkspace?.name ? truncateText(activeWorkspace.name, 14) : "Loading..."}
|
||||||
</h4>
|
</h4>
|
||||||
@ -243,7 +247,7 @@ export const WorkspaceSidebarDropdown = () => {
|
|||||||
</Transition>
|
</Transition>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
{!sidebarCollapse && (
|
{!store?.theme?.sidebarCollapsed && (
|
||||||
<Menu as="div" className="relative flex-shrink-0">
|
<Menu as="div" className="relative flex-shrink-0">
|
||||||
<Menu.Button className="grid place-items-center outline-none">
|
<Menu.Button className="grid place-items-center outline-none">
|
||||||
<Avatar user={user} height="28px" width="28px" fontSize="14px" />
|
<Avatar user={user} height="28px" width="28px" fontSize="14px" />
|
||||||
|
@ -16,6 +16,8 @@ import {
|
|||||||
TaskAltOutlined,
|
TaskAltOutlined,
|
||||||
WorkOutlineOutlined,
|
WorkOutlineOutlined,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
const workspaceLinks = (workspaceSlug: string) => [
|
const workspaceLinks = (workspaceSlug: string) => [
|
||||||
{
|
{
|
||||||
@ -41,6 +43,8 @@ const workspaceLinks = (workspaceSlug: string) => [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const WorkspaceSidebarMenu = () => {
|
export const WorkspaceSidebarMenu = () => {
|
||||||
|
const store: any = useMobxStore();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
@ -61,17 +65,17 @@ export const WorkspaceSidebarMenu = () => {
|
|||||||
tooltipContent={link.name}
|
tooltipContent={link.name}
|
||||||
position="right"
|
position="right"
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
disabled={!sidebarCollapse}
|
disabled={!store?.theme?.sidebarCollapsed}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
||||||
isActive
|
isActive
|
||||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
||||||
} ${sidebarCollapse ? "justify-center" : ""}`}
|
} ${store?.theme?.sidebarCollapsed ? "justify-center" : ""}`}
|
||||||
>
|
>
|
||||||
{<link.Icon fontSize="small" />}
|
{<link.Icon fontSize="small" />}
|
||||||
{!sidebarCollapse && link.name}
|
{!store?.theme?.sidebarCollapsed && link.name}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</a>
|
</a>
|
||||||
|
@ -7,20 +7,25 @@ import {
|
|||||||
WorkspaceSidebarMenu,
|
WorkspaceSidebarMenu,
|
||||||
} from "components/workspace";
|
} from "components/workspace";
|
||||||
import { ProjectSidebarList } from "components/project";
|
import { ProjectSidebarList } from "components/project";
|
||||||
|
// mobx react lite
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
export interface SidebarProps {
|
export interface SidebarProps {
|
||||||
toggleSidebar: boolean;
|
toggleSidebar: boolean;
|
||||||
setToggleSidebar: React.Dispatch<React.SetStateAction<boolean>>;
|
setToggleSidebar: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sidebar: React.FC<SidebarProps> = ({ toggleSidebar, setToggleSidebar }) => {
|
const Sidebar: React.FC<SidebarProps> = observer(({ toggleSidebar, setToggleSidebar }) => {
|
||||||
|
const store: any = useMobxStore();
|
||||||
// theme
|
// theme
|
||||||
const { collapsed: sidebarCollapse } = useTheme();
|
const { collapsed: sidebarCollapse } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`fixed md:relative inset-y-0 flex flex-col bg-custom-sidebar-background-100 h-full flex-shrink-0 flex-grow-0 border-r border-custom-sidebar-border-200 z-20 duration-300 ${
|
className={`fixed md:relative inset-y-0 flex flex-col bg-custom-sidebar-background-100 h-full flex-shrink-0 flex-grow-0 border-r border-custom-sidebar-border-200 z-20 duration-300 ${
|
||||||
sidebarCollapse ? "" : "md:w-[280px]"
|
store?.theme?.sidebarCollapsed ? "" : "md:w-[280px]"
|
||||||
} ${toggleSidebar ? "left-0" : "-left-full md:left-0"}`}
|
} ${toggleSidebar ? "left-0" : "-left-full md:left-0"}`}
|
||||||
>
|
>
|
||||||
<div className="flex h-full w-full flex-1 flex-col">
|
<div className="flex h-full w-full flex-1 flex-col">
|
||||||
@ -31,6 +36,6 @@ const Sidebar: React.FC<SidebarProps> = ({ toggleSidebar, setToggleSidebar }) =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default Sidebar;
|
export default Sidebar;
|
||||||
|
33
apps/app/lib/mobx/store-init.tsx
Normal file
33
apps/app/lib/mobx/store-init.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
|
const MobxStoreInit = () => {
|
||||||
|
const store: any = useMobxStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// sidebar collapsed toggle
|
||||||
|
if (
|
||||||
|
localStorage &&
|
||||||
|
localStorage.getItem("app_sidebar_collapsed") &&
|
||||||
|
store?.theme?.sidebarCollapsed === null
|
||||||
|
)
|
||||||
|
store.theme.setSidebarCollapsed(
|
||||||
|
localStorage.getItem("app_sidebar_collapsed")
|
||||||
|
? localStorage.getItem("app_sidebar_collapsed") === "true"
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
: false
|
||||||
|
);
|
||||||
|
|
||||||
|
// theme
|
||||||
|
if (localStorage && localStorage.getItem("theme") && store.theme.theme === null)
|
||||||
|
store.theme.setTheme(
|
||||||
|
localStorage.getItem("theme") ? localStorage.getItem("theme") : "system"
|
||||||
|
);
|
||||||
|
}, [store?.theme]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobxStoreInit;
|
30
apps/app/lib/mobx/store-provider.tsx
Normal file
30
apps/app/lib/mobx/store-provider.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { createContext, useContext } from "react";
|
||||||
|
// mobx store
|
||||||
|
import { RootStore } from "store/root";
|
||||||
|
|
||||||
|
let rootStore: any = null;
|
||||||
|
|
||||||
|
export const MobxStoreContext = createContext(null);
|
||||||
|
|
||||||
|
const initializeStore = () => {
|
||||||
|
const _rootStore = rootStore ?? new RootStore();
|
||||||
|
|
||||||
|
if (typeof window === "undefined") return _rootStore;
|
||||||
|
|
||||||
|
if (!rootStore) rootStore = _rootStore;
|
||||||
|
|
||||||
|
return _rootStore;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MobxStoreProvider = ({ children }: any) => {
|
||||||
|
const store = initializeStore();
|
||||||
|
|
||||||
|
return <MobxStoreContext.Provider value={store}>{children}</MobxStoreContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// hook
|
||||||
|
export const useMobxStore = () => {
|
||||||
|
const context = useContext(MobxStoreContext);
|
||||||
|
if (context === undefined) throw new Error("useMobxStore must be used within MobxStoreProvider");
|
||||||
|
return context;
|
||||||
|
};
|
@ -36,6 +36,8 @@
|
|||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"mobx": "^6.10.0",
|
||||||
|
"mobx-react-lite": "^4.0.3",
|
||||||
"next": "12.3.2",
|
"next": "12.3.2",
|
||||||
"next-pwa": "^5.6.0",
|
"next-pwa": "^5.6.0",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
|
@ -33,6 +33,9 @@ import {
|
|||||||
SITE_KEYWORDS,
|
SITE_KEYWORDS,
|
||||||
SITE_TITLE,
|
SITE_TITLE,
|
||||||
} from "constants/seo-variables";
|
} from "constants/seo-variables";
|
||||||
|
// mobx store provider
|
||||||
|
import { MobxStoreProvider } from "lib/mobx/store-provider";
|
||||||
|
import MobxStoreInit from "lib/mobx/store-init";
|
||||||
|
|
||||||
const CrispWithNoSSR = dynamic(() => import("constants/crisp"), { ssr: false });
|
const CrispWithNoSSR = dynamic(() => import("constants/crisp"), { ssr: false });
|
||||||
|
|
||||||
@ -45,9 +48,10 @@ Router.events.on("routeChangeComplete", NProgress.done);
|
|||||||
function MyApp({ Component, pageProps }: AppProps) {
|
function MyApp({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
// <UserProvider>
|
// <UserProvider>
|
||||||
<ThemeProvider themes={THEMES} defaultTheme="system">
|
// mobx root provider
|
||||||
<ToastContextProvider>
|
<MobxStoreProvider {...pageProps}>
|
||||||
<ThemeContextProvider>
|
<ThemeProvider themes={THEMES} defaultTheme="system">
|
||||||
|
<ToastContextProvider>
|
||||||
<CrispWithNoSSR />
|
<CrispWithNoSSR />
|
||||||
<Head>
|
<Head>
|
||||||
<title>{SITE_TITLE}</title>
|
<title>{SITE_TITLE}</title>
|
||||||
@ -64,10 +68,11 @@ function MyApp({ Component, pageProps }: AppProps) {
|
|||||||
<link rel="manifest" href="/site.webmanifest.json" />
|
<link rel="manifest" href="/site.webmanifest.json" />
|
||||||
<link rel="shortcut icon" href="/favicon/favicon.ico" />
|
<link rel="shortcut icon" href="/favicon/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
|
<MobxStoreInit />
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ThemeContextProvider>
|
</ToastContextProvider>
|
||||||
</ToastContextProvider>
|
</ThemeProvider>
|
||||||
</ThemeProvider>
|
</MobxStoreProvider>
|
||||||
// </UserProvider>
|
// </UserProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
17
apps/app/store/root.ts
Normal file
17
apps/app/store/root.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// mobx lite
|
||||||
|
import { enableStaticRendering } from "mobx-react-lite";
|
||||||
|
// store imports
|
||||||
|
import UserStore from "./user";
|
||||||
|
import ThemeStore from "./theme";
|
||||||
|
|
||||||
|
enableStaticRendering(typeof window === "undefined");
|
||||||
|
|
||||||
|
export class RootStore {
|
||||||
|
user;
|
||||||
|
theme;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.user = new UserStore(this);
|
||||||
|
this.theme = new ThemeStore(this);
|
||||||
|
}
|
||||||
|
}
|
66
apps/app/store/theme.ts
Normal file
66
apps/app/store/theme.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// mobx
|
||||||
|
import { action, observable, makeObservable } from "mobx";
|
||||||
|
// helper
|
||||||
|
import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper";
|
||||||
|
// interfaces
|
||||||
|
import { ICurrentUserSettings } from "types";
|
||||||
|
|
||||||
|
class ThemeStore {
|
||||||
|
sidebarCollapsed: boolean | null = null;
|
||||||
|
theme: string | null = null;
|
||||||
|
// root store
|
||||||
|
rootStore;
|
||||||
|
|
||||||
|
constructor(_rootStore: any | null = null) {
|
||||||
|
makeObservable(this, {
|
||||||
|
// observable
|
||||||
|
sidebarCollapsed: observable,
|
||||||
|
theme: observable,
|
||||||
|
// action
|
||||||
|
setSidebarCollapsed: action,
|
||||||
|
setTheme: action,
|
||||||
|
// computed
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rootStore = _rootStore;
|
||||||
|
this.initialLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
setSidebarCollapsed(collapsed: boolean | null = null) {
|
||||||
|
if (collapsed === null) {
|
||||||
|
let _sidebarCollapsed: string | boolean | null =
|
||||||
|
localStorage.getItem("app_sidebar_collapsed");
|
||||||
|
_sidebarCollapsed = _sidebarCollapsed ? (_sidebarCollapsed === "true" ? true : false) : false;
|
||||||
|
this.sidebarCollapsed = _sidebarCollapsed;
|
||||||
|
} else {
|
||||||
|
this.sidebarCollapsed = collapsed;
|
||||||
|
localStorage.setItem("app_sidebar_collapsed", collapsed.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme = async (_theme: ICurrentUserSettings) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem("theme", _theme.theme.toString());
|
||||||
|
this.theme = _theme.theme.toString();
|
||||||
|
|
||||||
|
if (this.theme === "custom") {
|
||||||
|
let themeSettings = this.rootStore.user.currentUserSettings || null;
|
||||||
|
if (themeSettings && themeSettings.theme.palette) {
|
||||||
|
applyTheme(
|
||||||
|
themeSettings.theme.palette !== ",,,,"
|
||||||
|
? themeSettings.theme.palette
|
||||||
|
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
|
||||||
|
themeSettings.theme.darkPalette
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else unsetCustomCssVariables();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("setting user theme error", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// init load
|
||||||
|
initialLoad() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThemeStore;
|
106
apps/app/store/user.ts
Normal file
106
apps/app/store/user.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
// mobx
|
||||||
|
import { action, observable, computed, makeObservable } from "mobx";
|
||||||
|
// services
|
||||||
|
import UserService from "services/user.service";
|
||||||
|
// interfaces
|
||||||
|
import { ICurrentUser, ICurrentUserSettings } from "types/users";
|
||||||
|
|
||||||
|
class UserStore {
|
||||||
|
currentUser: ICurrentUser | null = null;
|
||||||
|
currentUserSettings: ICurrentUserSettings | null = null;
|
||||||
|
// root store
|
||||||
|
rootStore;
|
||||||
|
|
||||||
|
constructor(_rootStore: any) {
|
||||||
|
makeObservable(this, {
|
||||||
|
// observable
|
||||||
|
currentUser: observable.ref,
|
||||||
|
currentUserSettings: observable.ref,
|
||||||
|
// action
|
||||||
|
setCurrentUser: action,
|
||||||
|
setCurrentUserSettings: action,
|
||||||
|
updateCurrentUser: action,
|
||||||
|
updateCurrentUserSettings: action,
|
||||||
|
// computed
|
||||||
|
});
|
||||||
|
this.rootStore = _rootStore;
|
||||||
|
this.initialLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentUser = async () => {
|
||||||
|
try {
|
||||||
|
let userResponse: ICurrentUser | null = await UserService.currentUser();
|
||||||
|
userResponse = userResponse || null;
|
||||||
|
if (userResponse) {
|
||||||
|
this.currentUser = {
|
||||||
|
id: userResponse?.id,
|
||||||
|
avatar: userResponse?.avatar,
|
||||||
|
first_name: userResponse?.first_name,
|
||||||
|
last_name: userResponse?.last_name,
|
||||||
|
username: userResponse?.username,
|
||||||
|
email: userResponse?.email,
|
||||||
|
mobile_number: userResponse?.mobile_number,
|
||||||
|
is_email_verified: userResponse?.is_email_verified,
|
||||||
|
is_tour_completed: userResponse?.is_tour_completed,
|
||||||
|
onboarding_step: userResponse?.onboarding_step,
|
||||||
|
is_onboarded: userResponse?.is_onboarded,
|
||||||
|
role: userResponse?.role,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fetching current user error", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setCurrentUserSettings = async () => {
|
||||||
|
try {
|
||||||
|
let userSettingsResponse: ICurrentUserSettings | null = await UserService.currentUser();
|
||||||
|
userSettingsResponse = userSettingsResponse || null;
|
||||||
|
if (userSettingsResponse) {
|
||||||
|
this.currentUserSettings = {
|
||||||
|
theme: userSettingsResponse?.theme,
|
||||||
|
};
|
||||||
|
this.rootStore.theme.setTheme();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fetching current user error", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateCurrentUser = async (user: ICurrentUser) => {
|
||||||
|
try {
|
||||||
|
let userResponse: ICurrentUser = await UserService.updateUser(user);
|
||||||
|
userResponse = userResponse || null;
|
||||||
|
if (userResponse) {
|
||||||
|
this.currentUser = userResponse;
|
||||||
|
return userResponse;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Updating user error", error);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateCurrentUserSettings = async (userTheme: ICurrentUserSettings) => {
|
||||||
|
try {
|
||||||
|
let userSettingsResponse: ICurrentUserSettings = await UserService.updateUser(userTheme);
|
||||||
|
userSettingsResponse = userSettingsResponse || null;
|
||||||
|
if (userSettingsResponse) {
|
||||||
|
this.currentUserSettings = userSettingsResponse;
|
||||||
|
this.rootStore.theme.setTheme(userTheme);
|
||||||
|
return userSettingsResponse;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Updating user settings error", error);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// init load
|
||||||
|
initialLoad() {
|
||||||
|
this.setCurrentUser();
|
||||||
|
this.setCurrentUserSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserStore;
|
41
apps/app/types/users.d.ts
vendored
41
apps/app/types/users.d.ts
vendored
@ -38,17 +38,6 @@ export interface IUser {
|
|||||||
[...rest: string]: any;
|
[...rest: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICustomTheme {
|
|
||||||
background: string;
|
|
||||||
text: string;
|
|
||||||
primary: string;
|
|
||||||
sidebarBackground: string;
|
|
||||||
sidebarText: string;
|
|
||||||
darkPalette: boolean;
|
|
||||||
palette: string;
|
|
||||||
theme: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICurrentUserResponse extends IUser {
|
export interface ICurrentUserResponse extends IUser {
|
||||||
assigned_issues: number;
|
assigned_issues: number;
|
||||||
last_workspace_id: string | null;
|
last_workspace_id: string | null;
|
||||||
@ -158,3 +147,33 @@ export interface IUserProfileProjectSegregation {
|
|||||||
user_timezone: string;
|
user_timezone: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ICurrentUser {
|
||||||
|
id: readonly string;
|
||||||
|
avatar: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
mobile_number: string;
|
||||||
|
is_email_verified: boolean;
|
||||||
|
is_tour_completed: boolean;
|
||||||
|
onboarding_step: TOnboardingSteps;
|
||||||
|
is_onboarded: boolean;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICustomTheme {
|
||||||
|
background: string;
|
||||||
|
text: string;
|
||||||
|
primary: string;
|
||||||
|
sidebarBackground: string;
|
||||||
|
sidebarText: string;
|
||||||
|
darkPalette: boolean;
|
||||||
|
palette: string;
|
||||||
|
theme: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICurrentUserSettings {
|
||||||
|
theme: ICustomTheme;
|
||||||
|
}
|
||||||
|
12
yarn.lock
12
yarn.lock
@ -6673,6 +6673,18 @@ mkdirp@^0.5.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.6"
|
minimist "^1.2.6"
|
||||||
|
|
||||||
|
mobx-react-lite@^4.0.3:
|
||||||
|
version "4.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-4.0.3.tgz#f7aa5ac3be558ca19a53b2929d9599679769c2a8"
|
||||||
|
integrity sha512-wEE1oT5zvDdvplG4HnRrFgPwg5GFVVrEtl42Er85k23zeu3om8H8wbDPgdbQP88zAihVsik6xJfw6VnzUl8fQw==
|
||||||
|
dependencies:
|
||||||
|
use-sync-external-store "^1.2.0"
|
||||||
|
|
||||||
|
mobx@^6.10.0:
|
||||||
|
version "6.10.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.10.0.tgz#3537680fe98d45232cc19cc8f76280bd8bb6b0b7"
|
||||||
|
integrity sha512-WMbVpCMFtolbB8swQ5E2YRrU+Yu8iLozCVx3CdGjbBKlP7dFiCSuiG06uea3JCFN5DnvtAX7+G5Bp82e2xu0ww==
|
||||||
|
|
||||||
moo@^0.5.1:
|
moo@^0.5.1:
|
||||||
version "0.5.2"
|
version "0.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c"
|
resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c"
|
||||||
|
Loading…
Reference in New Issue
Block a user