mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: handled seo and signout logic and new user popup
This commit is contained in:
parent
04df4bfd09
commit
ccf9f4d611
2
admin/.env.example
Normal file
2
admin/.env.example
Normal file
@ -0,0 +1,2 @@
|
||||
NEXT_PUBLIC_APP_URL=
|
||||
NEXT_PUBLIC_API_BASE_URL=
|
@ -5,6 +5,8 @@ import { ThemeProvider } from "next-themes";
|
||||
// lib
|
||||
import { StoreProvider } from "@/lib/store-context";
|
||||
import { AppWrapper } from "@/lib/wrappers";
|
||||
// constants
|
||||
import { SITE_NAME, SITE_DESCRIPTION, SITE_URL, TWITTER_USER_NAME, SITE_KEYWORDS, SITE_TITLE } from "@/constants/seo";
|
||||
// styles
|
||||
import "./globals.css";
|
||||
|
||||
@ -12,16 +14,35 @@ interface RootLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => (
|
||||
<html lang="en">
|
||||
<body className={`antialiased`}>
|
||||
<StoreProvider {...pageProps}>
|
||||
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
|
||||
<AppWrapper>{children}</AppWrapper>
|
||||
</ThemeProvider>
|
||||
</StoreProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => {
|
||||
const prefix = parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0") === 0 ? "/" : "/god-mode/";
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{SITE_TITLE}</title>
|
||||
<meta property="og:site_name" content={SITE_NAME} />
|
||||
<meta property="og:title" content={SITE_TITLE} />
|
||||
<meta property="og:url" content={SITE_URL} />
|
||||
<meta name="description" content={SITE_DESCRIPTION} />
|
||||
<meta property="og:description" content={SITE_DESCRIPTION} />
|
||||
<meta name="keywords" content={SITE_KEYWORDS} />
|
||||
<meta name="twitter:site" content={`@${TWITTER_USER_NAME}`} />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={`${prefix}favicon/apple-touch-icon.png`} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={`${prefix}favicon/favicon-32x32.png`} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={`${prefix}favicon/favicon-16x16.png`} />
|
||||
<link rel="manifest" href={`${prefix}site.webmanifest.json`} />
|
||||
<link rel="shortcut icon" href={`${prefix}favicon/favicon.ico`} />
|
||||
</head>
|
||||
<body className={`antialiased`}>
|
||||
<StoreProvider {...pageProps}>
|
||||
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
|
||||
<AppWrapper>{children}</AppWrapper>
|
||||
</ThemeProvider>
|
||||
</StoreProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default RootLayout;
|
||||
|
@ -4,10 +4,9 @@ import { FC, useState, useRef } from "react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import Link from "next/link";
|
||||
import { FileText, HelpCircle, MoveLeft } from "lucide-react";
|
||||
import { DiscordIcon, GithubIcon, getButtonStyling } from "@plane/ui";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks";
|
||||
// icons
|
||||
import { DiscordIcon, GithubIcon } from "@plane/ui";
|
||||
// assets
|
||||
import packageJson from "package.json";
|
||||
|
||||
@ -44,8 +43,14 @@ export const HelpSection: FC = () => {
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full justify-end"}`}
|
||||
className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full justify-between"}`}
|
||||
>
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : "/"}`}
|
||||
className={getButtonStyling("outline-primary", "sm")}
|
||||
>
|
||||
Go to plane
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
|
||||
|
@ -1,38 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { useTheme as useNextTheme } from "next-themes";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { LogOut, UserCog2, Palette } from "lucide-react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { Avatar, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Avatar } from "@plane/ui";
|
||||
// hooks
|
||||
import { useTheme, useUser } from "@/hooks";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// services
|
||||
import { AuthService } from "@/services";
|
||||
|
||||
// service initialization
|
||||
const authService = new AuthService();
|
||||
|
||||
export const SidebarDropdown = observer(() => {
|
||||
// store hooks
|
||||
const { isSidebarCollapsed } = useTheme();
|
||||
const { currentUser, signOut } = useUser();
|
||||
const { currentUser } = useUser();
|
||||
// hooks
|
||||
const { resolvedTheme, setTheme } = useNextTheme();
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await signOut().catch(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Failed to sign out. Please try again.",
|
||||
})
|
||||
);
|
||||
};
|
||||
// state
|
||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||
|
||||
const handleThemeSwitch = () => {
|
||||
const newTheme = resolvedTheme === "dark" ? "light" : "dark";
|
||||
setTheme(newTheme);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (csrfToken === undefined)
|
||||
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
||||
}, [csrfToken]);
|
||||
|
||||
return (
|
||||
<div className="flex max-h-[3.75rem] items-center gap-x-5 gap-y-2 border-b border-custom-sidebar-border-200 px-4 py-3.5">
|
||||
<div className="h-full w-full truncate">
|
||||
@ -94,11 +96,11 @@ export const SidebarDropdown = observer(() => {
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<form method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-out/`}>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="button"
|
||||
type="submit"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||||
Sign out
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Head from "next/head";
|
||||
"use client";
|
||||
|
||||
type TPageHeader = {
|
||||
title?: string;
|
||||
@ -9,9 +9,9 @@ export const PageHeader: React.FC<TPageHeader> = (props) => {
|
||||
const { title = "God Mode - Plane", description = "Plane god mode" } = props;
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
</Head>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -2,9 +2,8 @@
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTheme as nextUseTheme } from "next-themes";
|
||||
// ui
|
||||
import { Button, getButtonStyling } from "@plane/ui";
|
||||
// helpers
|
||||
@ -12,25 +11,17 @@ import { resolveGeneralTheme } from "helpers/common.helper";
|
||||
// icons
|
||||
import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg";
|
||||
import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg";
|
||||
import { useTheme } from "@/hooks";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
export const CreateWorkspacePopup: React.FC<Props> = observer((props) => {
|
||||
const { isOpen, onClose } = props;
|
||||
export const NewUserPopup: React.FC = observer(() => {
|
||||
// hooks
|
||||
const { isNewUserPopup, toggleNewUserPopup } = useTheme();
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const handleClose = () => {
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
const { resolvedTheme } = nextUseTheme();
|
||||
|
||||
if (!isNewUserPopup) return <></>;
|
||||
return (
|
||||
<div className="absolute bottom-8 right-6 p-6 w-96 border border-custom-border-100 shadow-md rounded-xl bg-custom-background-100 z-20">
|
||||
<div className="absolute bottom-8 right-8 p-6 w-96 border border-custom-border-100 shadow-md rounded-lg bg-custom-background-100">
|
||||
<div className="flex gap-4">
|
||||
<div className="grow">
|
||||
<div className="text-base font-semibold">Create workspace</div>
|
||||
@ -39,10 +30,13 @@ export const CreateWorkspacePopup: React.FC<Props> = observer((props) => {
|
||||
workspace, you will need to login again.
|
||||
</div>
|
||||
<div className="flex items-center gap-4 pt-2">
|
||||
<Link href="/create-workspace" className={getButtonStyling("primary", "sm")}>
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : "/"}`}
|
||||
className={getButtonStyling("primary", "sm")}
|
||||
>
|
||||
Create workspace
|
||||
</Link>
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
</a>
|
||||
<Button variant="neutral-primary" size="sm" onClick={toggleNewUserPopup}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
8
admin/constants/seo.ts
Normal file
8
admin/constants/seo.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const SITE_NAME = "Plane | Simple, extensible, open-source project management tool.";
|
||||
export const SITE_TITLE = "Plane | Simple, extensible, open-source project management tool.";
|
||||
export const SITE_DESCRIPTION =
|
||||
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.";
|
||||
export const SITE_KEYWORDS =
|
||||
"software development, plan, ship, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration";
|
||||
export const SITE_URL = "https://app.plane.so/";
|
||||
export const TWITTER_USER_NAME = "Plane | Simple, extensible, open-source project management tool.";
|
@ -1,6 +1,8 @@
|
||||
import { FC, ReactNode } from "react";
|
||||
// components
|
||||
import { InstanceSidebar } from "@/components/admin-sidebar";
|
||||
import { InstanceHeader } from "@/components/auth-header";
|
||||
import { NewUserPopup } from "@/components/new-user-popup";
|
||||
|
||||
type TAdminLayout = {
|
||||
children: ReactNode;
|
||||
@ -16,6 +18,7 @@ export const AdminLayout: FC<TAdminLayout> = (props) => {
|
||||
<InstanceHeader />
|
||||
<div className="h-full w-full overflow-hidden">{children}</div>
|
||||
</main>
|
||||
<NewUserPopup />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1 +1,11 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{ "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
13
admin/public/site.webmanifest.json
Normal file
13
admin/public/site.webmanifest.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "Plane God Mode",
|
||||
"short_name": "Plane God Mode",
|
||||
"description": "Plane helps you plan your issues, cycles, and product modules.",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#f9fafb",
|
||||
"theme_color": "#3f76ff",
|
||||
"icons": [
|
||||
{ "src": "/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
|
||||
]
|
||||
}
|
@ -19,27 +19,4 @@ export class AuthService extends APIService {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
async signOut(baseUrl: string): Promise<any> {
|
||||
await this.requestCSRFToken().then((data) => {
|
||||
const csrfToken = data?.csrf_token;
|
||||
|
||||
if (!csrfToken) throw Error("CSRF token not found");
|
||||
|
||||
var form = document.createElement("form");
|
||||
var element1 = document.createElement("input");
|
||||
|
||||
form.method = "POST";
|
||||
form.action = `${baseUrl}/api/instances/admins/sign-out/`;
|
||||
|
||||
element1.value = csrfToken;
|
||||
element1.name = "csrfmiddlewaretoken";
|
||||
element1.type = "hidden";
|
||||
form.appendChild(element1);
|
||||
|
||||
document.body.appendChild(form);
|
||||
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -75,6 +75,8 @@ export class InstanceStore implements IInstanceStore {
|
||||
try {
|
||||
if (this.instance === undefined) this.isLoading = true;
|
||||
const instance = await this.instanceService.getInstanceInfo();
|
||||
// handling the new user popup toggle
|
||||
if (this.instance === undefined && !instance?.instance?.workspaces_exist) this.store.theme.toggleNewUserPopup();
|
||||
runInAction(() => {
|
||||
this.isLoading = false;
|
||||
this.instance = instance;
|
||||
|
@ -5,31 +5,41 @@ import { RootStore } from "@/store/root-store";
|
||||
type TTheme = "dark" | "light";
|
||||
export interface IThemeStore {
|
||||
// observables
|
||||
isNewUserPopup: boolean;
|
||||
theme: string | undefined;
|
||||
isSidebarCollapsed: boolean | undefined;
|
||||
// actions
|
||||
toggleNewUserPopup: () => void;
|
||||
toggleSidebar: (collapsed: boolean) => void;
|
||||
setTheme: (currentTheme: TTheme) => void;
|
||||
}
|
||||
|
||||
export class ThemeStore implements IThemeStore {
|
||||
// observables
|
||||
isNewUserPopup: boolean = false;
|
||||
isSidebarCollapsed: boolean | undefined = undefined;
|
||||
theme: string | undefined = undefined;
|
||||
|
||||
constructor(private store: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
isNewUserPopup: observable.ref,
|
||||
isSidebarCollapsed: observable.ref,
|
||||
theme: observable.ref,
|
||||
// action
|
||||
toggleNewUserPopup: action,
|
||||
toggleSidebar: action,
|
||||
setTheme: action,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the sidebar collapsed state
|
||||
* @description Toggle the new user popup modal
|
||||
*/
|
||||
toggleNewUserPopup = () => (this.isNewUserPopup = !this.isNewUserPopup);
|
||||
|
||||
/**
|
||||
* @description Toggle the sidebar collapsed state
|
||||
* @param isCollapsed
|
||||
*/
|
||||
toggleSidebar = (isCollapsed: boolean) => {
|
||||
@ -39,7 +49,7 @@ export class ThemeStore implements IThemeStore {
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the user theme and applies it to the platform
|
||||
* @description Sets the user theme and applies it to the platform
|
||||
* @param currentTheme
|
||||
*/
|
||||
setTheme = async (currentTheme: TTheme) => {
|
||||
|
@ -3,6 +3,7 @@
|
||||
"globalEnv": [
|
||||
"NODE_ENV",
|
||||
"NEXT_PUBLIC_API_BASE_URL",
|
||||
"NEXT_PUBLIC_APP_URL",
|
||||
"NEXT_PUBLIC_DEPLOY_URL",
|
||||
"NEXT_PUBLIC_GOD_MODE_URL",
|
||||
"NEXT_PUBLIC_SENTRY_DSN",
|
||||
|
Loading…
Reference in New Issue
Block a user