chore: handled seo and signout logic and new user popup

This commit is contained in:
gurusainath 2024-05-03 14:50:55 +05:30
parent 04df4bfd09
commit ccf9f4d611
14 changed files with 126 additions and 78 deletions

2
admin/.env.example Normal file
View File

@ -0,0 +1,2 @@
NEXT_PUBLIC_APP_URL=
NEXT_PUBLIC_API_BASE_URL=

View File

@ -5,6 +5,8 @@ import { ThemeProvider } from "next-themes";
// lib // lib
import { StoreProvider } from "@/lib/store-context"; import { StoreProvider } from "@/lib/store-context";
import { AppWrapper } from "@/lib/wrappers"; import { AppWrapper } from "@/lib/wrappers";
// constants
import { SITE_NAME, SITE_DESCRIPTION, SITE_URL, TWITTER_USER_NAME, SITE_KEYWORDS, SITE_TITLE } from "@/constants/seo";
// styles // styles
import "./globals.css"; import "./globals.css";
@ -12,16 +14,35 @@ interface RootLayoutProps {
children: ReactNode; children: ReactNode;
} }
const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => ( const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => {
<html lang="en"> const prefix = parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0") === 0 ? "/" : "/god-mode/";
<body className={`antialiased`}>
<StoreProvider {...pageProps}> return (
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem> <html lang="en">
<AppWrapper>{children}</AppWrapper> <head>
</ThemeProvider> <title>{SITE_TITLE}</title>
</StoreProvider> <meta property="og:site_name" content={SITE_NAME} />
</body> <meta property="og:title" content={SITE_TITLE} />
</html> <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; export default RootLayout;

View File

@ -4,10 +4,9 @@ import { FC, useState, useRef } from "react";
import { Transition } from "@headlessui/react"; import { Transition } from "@headlessui/react";
import Link from "next/link"; import Link from "next/link";
import { FileText, HelpCircle, MoveLeft } from "lucide-react"; import { FileText, HelpCircle, MoveLeft } from "lucide-react";
import { DiscordIcon, GithubIcon, getButtonStyling } from "@plane/ui";
// hooks // hooks
import { useTheme } from "@/hooks"; import { useTheme } from "@/hooks";
// icons
import { DiscordIcon, GithubIcon } from "@plane/ui";
// assets // assets
import packageJson from "package.json"; import packageJson from "package.json";
@ -44,8 +43,14 @@ export const HelpSection: FC = () => {
}`} }`}
> >
<div <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 <button
type="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 ${ 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 ${

View File

@ -1,38 +1,40 @@
"use client"; "use client";
import { Fragment } from "react"; import { Fragment, useEffect, useState } from "react";
import { useTheme as useNextTheme } from "next-themes"; import { useTheme as useNextTheme } from "next-themes";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { LogOut, UserCog2, Palette } from "lucide-react"; import { LogOut, UserCog2, Palette } from "lucide-react";
import { Menu, Transition } from "@headlessui/react"; import { Menu, Transition } from "@headlessui/react";
import { Avatar, TOAST_TYPE, setToast } from "@plane/ui"; import { Avatar } from "@plane/ui";
// hooks // hooks
import { useTheme, useUser } from "@/hooks"; import { useTheme, useUser } from "@/hooks";
// helpers // helpers
import { API_BASE_URL } from "@/helpers/common.helper"; import { API_BASE_URL } from "@/helpers/common.helper";
// services
import { AuthService } from "@/services";
// service initialization
const authService = new AuthService();
export const SidebarDropdown = observer(() => { export const SidebarDropdown = observer(() => {
// store hooks // store hooks
const { isSidebarCollapsed } = useTheme(); const { isSidebarCollapsed } = useTheme();
const { currentUser, signOut } = useUser(); const { currentUser } = useUser();
// hooks // hooks
const { resolvedTheme, setTheme } = useNextTheme(); const { resolvedTheme, setTheme } = useNextTheme();
// state
const handleSignOut = async () => { const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
await signOut().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Failed to sign out. Please try again.",
})
);
};
const handleThemeSwitch = () => { const handleThemeSwitch = () => {
const newTheme = resolvedTheme === "dark" ? "light" : "dark"; const newTheme = resolvedTheme === "dark" ? "light" : "dark";
setTheme(newTheme); setTheme(newTheme);
}; };
useEffect(() => {
if (csrfToken === undefined)
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
}, [csrfToken]);
return ( 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="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"> <div className="h-full w-full truncate">
@ -94,11 +96,11 @@ export const SidebarDropdown = observer(() => {
</div> </div>
<div className="py-2"> <div className="py-2">
<form method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-out/`}> <form method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-out/`}>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<Menu.Item <Menu.Item
as="button" 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" 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]" /> <LogOut className="h-4 w-4 stroke-[1.5]" />
Sign out Sign out

View File

@ -1,4 +1,4 @@
import Head from "next/head"; "use client";
type TPageHeader = { type TPageHeader = {
title?: string; title?: string;
@ -9,9 +9,9 @@ export const PageHeader: React.FC<TPageHeader> = (props) => {
const { title = "God Mode - Plane", description = "Plane god mode" } = props; const { title = "God Mode - Plane", description = "Plane god mode" } = props;
return ( return (
<Head> <>
<title>{title}</title> <title>{title}</title>
<meta name="description" content={description} /> <meta name="description" content={description} />
</Head> </>
); );
}; };

View File

@ -2,9 +2,8 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { useTheme } from "next-themes"; import { useTheme as nextUseTheme } from "next-themes";
// ui // ui
import { Button, getButtonStyling } from "@plane/ui"; import { Button, getButtonStyling } from "@plane/ui";
// helpers // helpers
@ -12,25 +11,17 @@ import { resolveGeneralTheme } from "helpers/common.helper";
// icons // icons
import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg"; import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg";
import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg"; import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg";
import { useTheme } from "@/hooks";
type Props = { export const NewUserPopup: React.FC = observer(() => {
isOpen: boolean; // hooks
onClose?: () => void; const { isNewUserPopup, toggleNewUserPopup } = useTheme();
};
export const CreateWorkspacePopup: React.FC<Props> = observer((props) => {
const { isOpen, onClose } = props;
// theme // theme
const { resolvedTheme } = useTheme(); const { resolvedTheme } = nextUseTheme();
const handleClose = () => {
onClose && onClose();
};
if (!isOpen) return null;
if (!isNewUserPopup) return <></>;
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="flex gap-4">
<div className="grow"> <div className="grow">
<div className="text-base font-semibold">Create workspace</div> <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. workspace, you will need to login again.
</div> </div>
<div className="flex items-center gap-4 pt-2"> <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 Create workspace
</Link> </a>
<Button variant="neutral-primary" size="sm" onClick={handleClose}> <Button variant="neutral-primary" size="sm" onClick={toggleNewUserPopup}>
Close Close
</Button> </Button>
</div> </div>

8
admin/constants/seo.ts Normal file
View 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.";

View File

@ -1,6 +1,8 @@
import { FC, ReactNode } from "react"; import { FC, ReactNode } from "react";
// components
import { InstanceSidebar } from "@/components/admin-sidebar"; import { InstanceSidebar } from "@/components/admin-sidebar";
import { InstanceHeader } from "@/components/auth-header"; import { InstanceHeader } from "@/components/auth-header";
import { NewUserPopup } from "@/components/new-user-popup";
type TAdminLayout = { type TAdminLayout = {
children: ReactNode; children: ReactNode;
@ -16,6 +18,7 @@ export const AdminLayout: FC<TAdminLayout> = (props) => {
<InstanceHeader /> <InstanceHeader />
<div className="h-full w-full overflow-hidden">{children}</div> <div className="h-full w-full overflow-hidden">{children}</div>
</main> </main>
<NewUserPopup />
</div> </div>
); );
}; };

View File

@ -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"
}

View 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" }
]
}

View File

@ -19,27 +19,4 @@ export class AuthService extends APIService {
throw error; 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();
});
}
} }

View File

@ -75,6 +75,8 @@ export class InstanceStore implements IInstanceStore {
try { try {
if (this.instance === undefined) this.isLoading = true; if (this.instance === undefined) this.isLoading = true;
const instance = await this.instanceService.getInstanceInfo(); 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(() => { runInAction(() => {
this.isLoading = false; this.isLoading = false;
this.instance = instance; this.instance = instance;

View File

@ -5,31 +5,41 @@ import { RootStore } from "@/store/root-store";
type TTheme = "dark" | "light"; type TTheme = "dark" | "light";
export interface IThemeStore { export interface IThemeStore {
// observables // observables
isNewUserPopup: boolean;
theme: string | undefined; theme: string | undefined;
isSidebarCollapsed: boolean | undefined; isSidebarCollapsed: boolean | undefined;
// actions // actions
toggleNewUserPopup: () => void;
toggleSidebar: (collapsed: boolean) => void; toggleSidebar: (collapsed: boolean) => void;
setTheme: (currentTheme: TTheme) => void; setTheme: (currentTheme: TTheme) => void;
} }
export class ThemeStore implements IThemeStore { export class ThemeStore implements IThemeStore {
// observables // observables
isNewUserPopup: boolean = false;
isSidebarCollapsed: boolean | undefined = undefined; isSidebarCollapsed: boolean | undefined = undefined;
theme: string | undefined = undefined; theme: string | undefined = undefined;
constructor(private store: RootStore) { constructor(private store: RootStore) {
makeObservable(this, { makeObservable(this, {
// observables // observables
isNewUserPopup: observable.ref,
isSidebarCollapsed: observable.ref, isSidebarCollapsed: observable.ref,
theme: observable.ref, theme: observable.ref,
// action // action
toggleNewUserPopup: action,
toggleSidebar: action, toggleSidebar: action,
setTheme: 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 * @param isCollapsed
*/ */
toggleSidebar = (isCollapsed: boolean) => { 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 * @param currentTheme
*/ */
setTheme = async (currentTheme: TTheme) => { setTheme = async (currentTheme: TTheme) => {

View File

@ -3,6 +3,7 @@
"globalEnv": [ "globalEnv": [
"NODE_ENV", "NODE_ENV",
"NEXT_PUBLIC_API_BASE_URL", "NEXT_PUBLIC_API_BASE_URL",
"NEXT_PUBLIC_APP_URL",
"NEXT_PUBLIC_DEPLOY_URL", "NEXT_PUBLIC_DEPLOY_URL",
"NEXT_PUBLIC_GOD_MODE_URL", "NEXT_PUBLIC_GOD_MODE_URL",
"NEXT_PUBLIC_SENTRY_DSN", "NEXT_PUBLIC_SENTRY_DSN",