mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
feat: changemod space
This commit is contained in:
parent
243680132e
commit
9eac78ae83
@ -13,7 +13,7 @@ module.exports = {
|
||||
plugins: ["react", "@typescript-eslint", "import"],
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ["web/", "space/", "packages/*/"],
|
||||
rootDir: ["web/", "space/", "admin/", "packages/*/"],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
|
30
space/app/[workspace_slug]/[project_identifier]/layout.tsx
Normal file
30
space/app/[workspace_slug]/[project_identifier]/layout.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Image from "next/image";
|
||||
// components
|
||||
import IssueNavbar from "@/components/issues/navbar";
|
||||
// assets
|
||||
import planeLogo from "public/plane-logo.svg";
|
||||
|
||||
const ProjectLayout = ({ children }: { children: React.ReactNode }) => (
|
||||
<div className="relative flex h-screen min-h-[500px] w-screen flex-col overflow-hidden">
|
||||
<div className="relative flex h-[60px] flex-shrink-0 select-none items-center border-b border-custom-border-300 bg-custom-sidebar-background-100">
|
||||
<IssueNavbar />
|
||||
</div>
|
||||
<div className="relative h-full w-full overflow-hidden bg-custom-background-90">{children}</div>
|
||||
<a
|
||||
href="https://plane.so"
|
||||
className="fixed bottom-2.5 right-5 !z-[999999] flex items-center gap-1 rounded border border-custom-border-200 bg-custom-background-100 px-2 py-1 shadow-custom-shadow-2xs"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<div className="relative grid h-6 w-6 place-items-center">
|
||||
<Image src={planeLogo} alt="Plane logo" className="h-6 w-6" height="24" width="24" />
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
Powered by <span className="font-semibold">Plane Deploy</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default observer(ProjectLayout);
|
@ -35,9 +35,6 @@ const WorkspaceProjectPage = (props: any) => {
|
||||
return (
|
||||
<AuthWrapper pageType={EPageTypes.AUTHENTICATED}>
|
||||
<ProjectLayout>
|
||||
<Head>
|
||||
<title>{SITE_TITLE}</title>
|
||||
</Head>
|
||||
<ProjectDetailsView />
|
||||
</ProjectLayout>
|
||||
</AuthWrapper>
|
5
space/app/error.tsx
Normal file
5
space/app/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
|
||||
export default function InstanceError() {
|
||||
return <div>Instance Error</div>;
|
||||
}
|
49
space/app/layout.tsx
Normal file
49
space/app/layout.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { Metadata } from "next";
|
||||
// styles
|
||||
import "@/styles/globals.css";
|
||||
// components
|
||||
import { InstanceNotReady } from "@/components/instance";
|
||||
// lib
|
||||
import { AppProvider } from "@/lib/app-providers";
|
||||
// services
|
||||
import { InstanceService } from "@/services/instance.service";
|
||||
|
||||
const instanceService = new InstanceService();
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Plane Deploy | Make your Plane boards public with one-click",
|
||||
description: "Plane Deploy is a customer feedback management tool built on top of plane.so",
|
||||
openGraph: {
|
||||
title: "Plane Deploy | Make your Plane boards public with one-click",
|
||||
description: "Plane Deploy is a customer feedback management tool built on top of plane.so",
|
||||
url: "https://sites.plane.so/",
|
||||
},
|
||||
keywords:
|
||||
"software development, customer feedback, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration",
|
||||
twitter: {
|
||||
site: "@planepowers",
|
||||
},
|
||||
};
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const instanceDetails = await instanceService.getInstanceInfo();
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
{/* <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>
|
||||
{!instanceDetails?.instance?.is_setup_done ? (
|
||||
<InstanceNotReady />
|
||||
) : (
|
||||
<AppProvider initialState={{ instance: instanceDetails.instance }}>{children}</AppProvider>
|
||||
)}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
20
space/app/page.tsx
Normal file
20
space/app/page.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
// components
|
||||
import { AuthView } from "@/components/views";
|
||||
// helpers
|
||||
import { EPageTypes } from "@/helpers/authentication.helper";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// wrapper
|
||||
import { AuthWrapper } from "@/lib/wrappers";
|
||||
|
||||
export default function HomePage() {
|
||||
const { data } = useInstance();
|
||||
|
||||
console.log("data", data);
|
||||
|
||||
return (
|
||||
<AuthWrapper pageType={EPageTypes.INIT}>
|
||||
<AuthView />
|
||||
</AuthWrapper>
|
||||
);
|
||||
}
|
@ -1,40 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
// icons
|
||||
import { UserCog2 } from "lucide-react";
|
||||
// ui
|
||||
import { getButtonStyling } from "@plane/ui";
|
||||
import { Button } from "@plane/ui";
|
||||
// helper
|
||||
import { ADMIN_BASE_URL, ADMIN_BASE_PATH } from "@/helpers/common.helper";
|
||||
// images
|
||||
import instanceNotReady from "public/instance/plane-instance-not-ready.webp";
|
||||
import PlaneBlackLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
|
||||
import PlaneWhiteLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
|
||||
import PlaneTakeOffImage from "@/public/instance/plane-takeoff.png";
|
||||
|
||||
export const InstanceNotReady: FC = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const planeLogo = resolvedTheme === "dark" ? PlaneWhiteLogo : PlaneBlackLogo;
|
||||
const GOD_MODE_URL = encodeURI(ADMIN_BASE_URL + ADMIN_BASE_PATH);
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full overflow-y-auto bg-onboarding-gradient-100">
|
||||
<div className="h-full w-full pt-24">
|
||||
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-100 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3">
|
||||
<div className="relative h-full rounded-t-md bg-onboarding-gradient-200 px-7 sm:px-0">
|
||||
<div className="flex items-center justify-center py-10">
|
||||
<Image src={planeLogo} className="h-[44px] w-full" alt="Plane logo" />
|
||||
</div>
|
||||
<div className="mt-20">
|
||||
<Image src={instanceNotReady} className="w-full" alt="Instance not ready" />
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-center gap-5 py-12 pb-20">
|
||||
<h3 className="text-2xl font-medium">Your Plane instance isn{"'"}t ready yet</h3>
|
||||
<p className="text-sm">Ask your Instance Admin to complete set-up first.</p>
|
||||
<a href="/god-mode" className={`${getButtonStyling("primary", "md")} mt-4`}>
|
||||
<UserCog2 className="h-3.5 w-3.5" />
|
||||
Get started
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center pt-12">
|
||||
<div className="w-auto max-w-2xl relative space-y-8 py-10">
|
||||
<div className="relative flex flex-col justify-center items-center space-y-4">
|
||||
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
|
||||
<Image src={PlaneTakeOffImage} alt="Plane Logo" />
|
||||
<p className="font-medium text-base text-onboarding-text-400">
|
||||
Get started by setting up your instance and workspace
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href={GOD_MODE_URL}>
|
||||
<Button size="lg" className="w-full">
|
||||
Get started
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
|
@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Image from "next/image";
|
||||
// ui
|
||||
@ -17,7 +19,7 @@ export const AuthView = observer(() => {
|
||||
// hooks
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store
|
||||
const { data: currentUser, fetchCurrentUser, isLoading } = useUser();
|
||||
const { fetchCurrentUser, isLoading, isAuthenticated } = useUser();
|
||||
|
||||
// fetching user information
|
||||
const { isLoading: isSWRLoading } = useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
|
||||
@ -33,7 +35,7 @@ export const AuthView = observer(() => {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{currentUser ? (
|
||||
{isAuthenticated ? (
|
||||
<UserLoggedIn />
|
||||
) : (
|
||||
<div className="relative w-screen h-screen overflow-hidden">
|
||||
|
@ -3,4 +3,9 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
|
||||
|
||||
export const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL ?? "";
|
||||
export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH ?? "";
|
||||
|
||||
export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL ?? "";
|
||||
|
||||
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
|
||||
|
@ -1,4 +1,2 @@
|
||||
export * from "./user-mobx-provider";
|
||||
|
||||
export * from "./use-instance";
|
||||
export * from "./user";
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import { StoreContext } from "@/lib/app-providers";
|
||||
import { IInstanceStore } from "@/store/instance.store";
|
||||
|
||||
export const useInstance = (): IInstanceStore => {
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import { RootStore } from "@/store/root.store";
|
||||
|
||||
export const useMobxStore = (): RootStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useMobxStore must be used within StoreProvider");
|
||||
return context;
|
||||
};
|
@ -1,10 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import { StoreContext } from "@/lib/app-providers";
|
||||
import { IProfileStore } from "@/store/user/profile.store";
|
||||
|
||||
export const useUserProfile = (): IProfileStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider");
|
||||
return context.profile;
|
||||
return context.user.profile;
|
||||
};
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { useContext } from "react";
|
||||
// lib
|
||||
import { StoreContext } from "@/lib/app-providers";
|
||||
// store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import { IUserStore } from "@/store/user";
|
||||
import { IUserStore } from "@/store/user.store";
|
||||
|
||||
export const useUser = (): IUserStore => {
|
||||
const context = useContext(StoreContext);
|
||||
|
38
space/lib/app-providers.tsx
Normal file
38
space/lib/app-providers.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, createContext } from "react";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
// store
|
||||
import { RootStore } from "@/store/root.store";
|
||||
|
||||
let rootStore = new RootStore();
|
||||
|
||||
export const StoreContext = createContext(rootStore);
|
||||
|
||||
function initializeStore(initialData = {}) {
|
||||
const singletonRootStore = rootStore ?? new RootStore();
|
||||
// If your page has Next.js data fetching methods that use a Mobx store, it will
|
||||
// get hydrated here, check `pages/ssg.js` and `pages/ssr.js` for more details
|
||||
if (initialData) {
|
||||
singletonRootStore.hydrate(initialData);
|
||||
}
|
||||
// For SSG and SSR always create a new store
|
||||
if (typeof window === "undefined") return singletonRootStore;
|
||||
// Create the store once in the client
|
||||
if (!rootStore) rootStore = singletonRootStore;
|
||||
return singletonRootStore;
|
||||
}
|
||||
|
||||
export type AppProviderProps = {
|
||||
children: ReactNode;
|
||||
initialState: any;
|
||||
};
|
||||
|
||||
export const AppProvider = ({ children, initialState = {} }: AppProviderProps) => {
|
||||
const store = initializeStore(initialState);
|
||||
return (
|
||||
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
|
||||
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export const init = {};
|
@ -1,19 +0,0 @@
|
||||
import { ReactElement, createContext } from "react";
|
||||
// mobx store
|
||||
import { RootStore } from "@/store/root.store";
|
||||
|
||||
export let rootStore = new RootStore();
|
||||
|
||||
export const StoreContext = createContext<RootStore>(rootStore);
|
||||
|
||||
const initializeStore = () => {
|
||||
const singletonRootStore = rootStore ?? new RootStore();
|
||||
if (typeof window === "undefined") return singletonRootStore;
|
||||
if (!rootStore) rootStore = singletonRootStore;
|
||||
return singletonRootStore;
|
||||
};
|
||||
|
||||
export const StoreProvider = ({ children }: { children: ReactElement }) => {
|
||||
const store = initializeStore();
|
||||
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import { FC, ReactNode } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { useRouter } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// helpers
|
||||
|
@ -4,9 +4,9 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
"develop": "next dev -p 4000",
|
||||
"develop": "next dev -p 3002",
|
||||
"build": "next build",
|
||||
"start": "next start -p 4000",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"export": "next export"
|
||||
},
|
||||
|
@ -1,42 +0,0 @@
|
||||
// next imports
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Image from "next/image";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// images
|
||||
import notFoundImage from "public/404.svg";
|
||||
|
||||
const Custom404Error = observer(() => {
|
||||
// hooks
|
||||
const { instance } = useInstance();
|
||||
|
||||
const redirectionUrl = instance?.config?.app_base_url || "/";
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full min-h-screen w-screen items-center justify-center py-5">
|
||||
<div className="max-w-[700px] space-y-5">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<div className="relative h-[240px] w-[240px]">
|
||||
<Image src={notFoundImage} layout="fill" alt="404- Page not found" />
|
||||
</div>
|
||||
<div className="text-xl font-medium">Oops! Something went wrong.</div>
|
||||
<div className="text-sm text-custom-text-200">
|
||||
Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is
|
||||
temporarily unavailable.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center text-center">
|
||||
<a
|
||||
href={redirectionUrl}
|
||||
className="cursor-pointer select-none rounded-sm border border-gray-200 bg-gray-50 p-1.5 px-2.5 text-sm font-medium text-gray-700 transition-all hover:scale-105 hover:bg-gray-100 hover:text-gray-800"
|
||||
>
|
||||
Go to your Workspace
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default Custom404Error;
|
@ -1,5 +0,0 @@
|
||||
const WorkspaceProjectPage = () => (
|
||||
<div className="relative flex h-screen w-screen items-center justify-center text-5xl">Plane Workspace Space</div>
|
||||
);
|
||||
|
||||
export default WorkspaceProjectPage;
|
@ -1,47 +0,0 @@
|
||||
import type { AppProps } from "next/app";
|
||||
import Head from "next/head";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
// styles
|
||||
import "@/styles/globals.css";
|
||||
// contexts
|
||||
import { SITE_NAME, SITE_DESCRIPTION, SITE_URL, TWITTER_USER_NAME, SITE_KEYWORDS, SITE_TITLE } from "@/constants/seo";
|
||||
import { ToastContextProvider } from "@/contexts/toast.context";
|
||||
// mobx store provider
|
||||
import { StoreProvider } from "@/lib/store-context";
|
||||
// wrappers
|
||||
import { InstanceWrapper } from "@/lib/wrappers";
|
||||
|
||||
const prefix = "/spaces/";
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
<StoreProvider>
|
||||
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
|
||||
<ToastContextProvider>
|
||||
<InstanceWrapper>
|
||||
<Component {...pageProps} />
|
||||
</InstanceWrapper>
|
||||
</ToastContextProvider>
|
||||
</ThemeProvider>
|
||||
</StoreProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
@ -1,18 +0,0 @@
|
||||
import Document, { Html, Head, Main, NextScript } from "next/document";
|
||||
|
||||
class MyDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<body className="w-100 bg-custom-background-100 antialiased">
|
||||
<div id="context-menu-portal" />
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument;
|
@ -1,166 +0,0 @@
|
||||
import { NextPage } from "next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// icons
|
||||
import { CircleCheck } from "lucide-react";
|
||||
// ui
|
||||
import { Button, Input, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
|
||||
// helpers
|
||||
import { EPageTypes } from "@/helpers/authentication.helper";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { checkEmailValidity } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import useTimer from "@/hooks/use-timer";
|
||||
// wrappers
|
||||
import { AuthWrapper } from "@/lib/wrappers";
|
||||
// services
|
||||
import { AuthService } from "@/services/authentication.service";
|
||||
// images
|
||||
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
|
||||
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
|
||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||
|
||||
type TForgotPasswordFormValues = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
const defaultValues: TForgotPasswordFormValues = {
|
||||
email: "",
|
||||
};
|
||||
|
||||
// services
|
||||
const authService = new AuthService();
|
||||
|
||||
const ForgotPasswordPage: NextPage = () => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { email } = router.query;
|
||||
// hooks
|
||||
const { resolvedTheme } = useTheme();
|
||||
// timer
|
||||
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(0);
|
||||
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
handleSubmit,
|
||||
} = useForm<TForgotPasswordFormValues>({
|
||||
defaultValues: {
|
||||
...defaultValues,
|
||||
email: email?.toString() ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleForgotPassword = async (formData: TForgotPasswordFormValues) => {
|
||||
await authService
|
||||
.sendResetPasswordLink({
|
||||
email: formData.email,
|
||||
})
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Email sent",
|
||||
message:
|
||||
"Check your inbox for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder.",
|
||||
});
|
||||
setResendCodeTimer(30);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthWrapper pageType={EPageTypes.NON_AUTHENTICATED}>
|
||||
<div className="relative h-screen w-full overflow-hidden">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
||||
className="w-full h-full object-cover"
|
||||
alt="Plane background pattern"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
|
||||
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto h-full">
|
||||
<div className="h-full overflow-auto px-7 pb-56 pt-4 sm:px-0">
|
||||
<div className="mx-auto flex flex-col">
|
||||
<div className="text-center space-y-1 py-4 mx-auto sm:w-96">
|
||||
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
|
||||
Reset your password
|
||||
</h3>
|
||||
<p className="font-medium text-onboarding-text-400">
|
||||
Enter your user account{"'"}s verified email address and we will send you a password reset link.
|
||||
</p>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(handleForgotPassword)} className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="email"
|
||||
rules={{
|
||||
required: "Email is required",
|
||||
validate: (value) => checkEmailValidity(value) || "Email is invalid",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.email)}
|
||||
placeholder="name@company.com"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
disabled={resendTimerCode > 0}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{resendTimerCode > 0 && (
|
||||
<p className="flex w-full items-start px-1 gap-1 text-xs font-medium text-green-700">
|
||||
<CircleCheck height={12} width={12} className="mt-0.5" />
|
||||
We sent the reset link to your email address
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
disabled={!isValid}
|
||||
loading={isSubmitting || resendTimerCode > 0}
|
||||
>
|
||||
{resendTimerCode > 0 ? `Resend in ${resendTimerCode} seconds` : "Send reset link"}
|
||||
</Button>
|
||||
<Link href="/" className={cn("w-full", getButtonStyling("link-neutral", "lg"))}>
|
||||
Back to sign in
|
||||
</Link>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPasswordPage;
|
@ -1,205 +0,0 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
// icons
|
||||
import { useTheme } from "next-themes";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
// ui
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// components
|
||||
import { PasswordStrengthMeter } from "@/components/accounts";
|
||||
// helpers
|
||||
import { EPageTypes } from "@/helpers/authentication.helper";
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { getPasswordStrength } from "@/helpers/password.helper";
|
||||
// wrappers
|
||||
import { AuthWrapper } from "@/lib/wrappers";
|
||||
// services
|
||||
import { AuthService } from "@/services/authentication.service";
|
||||
// images
|
||||
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
|
||||
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
|
||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||
|
||||
type TResetPasswordFormValues = {
|
||||
email: string;
|
||||
password: string;
|
||||
confirm_password?: string;
|
||||
};
|
||||
|
||||
const defaultValues: TResetPasswordFormValues = {
|
||||
email: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
// services
|
||||
const authService = new AuthService();
|
||||
|
||||
const ResetPasswordPage: NextPage = () => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { uidb64, token, email } = router.query;
|
||||
// states
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [resetFormData, setResetFormData] = useState<TResetPasswordFormValues>({
|
||||
...defaultValues,
|
||||
email: email ? email.toString() : "",
|
||||
});
|
||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
|
||||
// hooks
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (email && !resetFormData.email) {
|
||||
setResetFormData((prev) => ({ ...prev, email: email.toString() }));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [email]);
|
||||
|
||||
const handleFormChange = (key: keyof TResetPasswordFormValues, value: string) =>
|
||||
setResetFormData((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
useEffect(() => {
|
||||
if (csrfToken === undefined)
|
||||
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
||||
}, [csrfToken]);
|
||||
|
||||
const isButtonDisabled = useMemo(
|
||||
() =>
|
||||
!!resetFormData.password &&
|
||||
getPasswordStrength(resetFormData.password) >= 3 &&
|
||||
resetFormData.password === resetFormData.confirm_password
|
||||
? false
|
||||
: true,
|
||||
[resetFormData]
|
||||
);
|
||||
|
||||
return (
|
||||
<AuthWrapper pageType={EPageTypes.NON_AUTHENTICATED}>
|
||||
<div className="relative h-screen w-full overflow-hidden">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
||||
className="w-full h-full object-cover"
|
||||
alt="Plane background pattern"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
|
||||
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto h-full">
|
||||
<div className="h-full overflow-auto px-7 pb-56 pt-4 sm:px-0">
|
||||
<div className="mx-auto flex flex-col">
|
||||
<div className="text-center space-y-1 py-4 mx-auto sm:w-96">
|
||||
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
|
||||
Set new password
|
||||
</h3>
|
||||
<p className="font-medium text-onboarding-text-400">Secure your account with a strong password</p>
|
||||
</div>
|
||||
<form
|
||||
className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96"
|
||||
method="POST"
|
||||
action={`${API_BASE_URL}/auth/reset-password/${uidb64?.toString()}/${token?.toString()}/`}
|
||||
>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={resetFormData.email}
|
||||
//hasError={Boolean(errors.email)}
|
||||
placeholder="name@company.com"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400 cursor-not-allowed"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
|
||||
Password
|
||||
</label>
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
name="password"
|
||||
value={resetFormData.password}
|
||||
onChange={(e) => handleFormChange("password", e.target.value)}
|
||||
//hasError={Boolean(errors.password)}
|
||||
placeholder="Enter password"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
minLength={8}
|
||||
onFocus={() => setIsPasswordInputFocused(true)}
|
||||
onBlur={() => setIsPasswordInputFocused(false)}
|
||||
autoFocus
|
||||
/>
|
||||
{showPassword ? (
|
||||
<EyeOff
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(false)}
|
||||
/>
|
||||
) : (
|
||||
<Eye
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isPasswordInputFocused && <PasswordStrengthMeter password={resetFormData.password} />}
|
||||
</div>
|
||||
{getPasswordStrength(resetFormData.password) >= 3 && (
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
|
||||
Confirm password
|
||||
</label>
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
name="confirm_password"
|
||||
value={resetFormData.confirm_password}
|
||||
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
|
||||
placeholder="Confirm password"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
/>
|
||||
{showPassword ? (
|
||||
<EyeOff
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(false)}
|
||||
/>
|
||||
) : (
|
||||
<Eye
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!!resetFormData.confirm_password &&
|
||||
resetFormData.password !== resetFormData.confirm_password && (
|
||||
<span className="text-sm text-red-500">Passwords don{"'"}t match</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
||||
Set password
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetPasswordPage;
|
@ -1,16 +0,0 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { NextPage } from "next";
|
||||
// components
|
||||
import { AuthView } from "@/components/views";
|
||||
// helpers
|
||||
import { EPageTypes } from "@/helpers/authentication.helper";
|
||||
// wrapper
|
||||
import { AuthWrapper } from "@/lib/wrappers";
|
||||
|
||||
const Index: NextPage = observer(() => (
|
||||
<AuthWrapper pageType={EPageTypes.INIT}>
|
||||
<AuthView />
|
||||
</AuthWrapper>
|
||||
));
|
||||
|
||||
export default Index;
|
@ -1,131 +0,0 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
// ui
|
||||
import { Avatar } from "@plane/ui";
|
||||
// components
|
||||
import { OnBoardingForm } from "@/components/accounts/onboarding-form";
|
||||
// helpers
|
||||
import { EPageTypes } from "@/helpers/authentication.helper";
|
||||
// hooks
|
||||
import { useUser, useUserProfile } from "@/hooks/store";
|
||||
// wrappers
|
||||
import { AuthWrapper } from "@/lib/wrappers";
|
||||
// assets
|
||||
import ProfileSetupDark from "public/onboarding/profile-setup-dark.svg";
|
||||
import ProfileSetup from "public/onboarding/profile-setup.svg";
|
||||
|
||||
const imagePrefix = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
|
||||
|
||||
const OnBoardingPage = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { next_path } = router.query;
|
||||
|
||||
// hooks
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const { data: user } = useUser();
|
||||
const { data: currentUserProfile, updateUserProfile } = useUserProfile();
|
||||
|
||||
if (!user) {
|
||||
router.push("/");
|
||||
return <></>;
|
||||
}
|
||||
|
||||
// complete onboarding
|
||||
const finishOnboarding = async () => {
|
||||
if (!user) return;
|
||||
|
||||
await updateUserProfile({
|
||||
onboarding_step: {
|
||||
...currentUserProfile?.onboarding_step,
|
||||
profile_complete: true,
|
||||
},
|
||||
}).catch(() => {
|
||||
console.log("Failed to update onboarding status");
|
||||
});
|
||||
|
||||
if (next_path) router.replace(next_path.toString());
|
||||
router.replace("/");
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthWrapper pageType={EPageTypes.ONBOARDING}>
|
||||
<div className="flex h-full w-full">
|
||||
<div className="w-full h-full overflow-auto px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex w-full items-center justify-between font-semibold ">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Image
|
||||
src={`${imagePrefix}/plane-logos/blue-without-text.png`}
|
||||
height={30}
|
||||
width={30}
|
||||
alt="Plane Logo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 lg:hidden">
|
||||
<div className="flex w-full shrink-0 justify-end">
|
||||
<div className="flex items-center gap-x-2 pr-4">
|
||||
{user?.avatar && (
|
||||
<Avatar
|
||||
name={user?.first_name ? `${user?.first_name} ${user?.last_name ?? ""}` : user?.email}
|
||||
src={user?.avatar}
|
||||
size={24}
|
||||
shape="square"
|
||||
fallbackBackgroundColor="#FCBE1D"
|
||||
className="!text-base capitalize"
|
||||
/>
|
||||
)}
|
||||
<span className="text-sm font-medium text-custom-text-200">
|
||||
{user?.first_name ? `${user?.first_name} ${user?.last_name ?? ""}` : user?.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-full items-center justify-center p-8 mt-14">
|
||||
<div className="text-center space-y-1 py-4 mx-auto">
|
||||
<h3 className="text-3xl font-bold text-onboarding-text-100">Welcome to Plane!</h3>
|
||||
<p className="font-medium text-onboarding-text-400">
|
||||
Let’s setup your profile, tell us a bit about yourself.
|
||||
</p>
|
||||
</div>
|
||||
<OnBoardingForm user={user} finishOnboarding={finishOnboarding} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden lg:block relative w-2/5 h-screen overflow-hidden px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28">
|
||||
<div className="flex w-full shrink-0 justify-end">
|
||||
<div className="flex items-center gap-x-2 pr-4 z-10">
|
||||
{user?.avatar && (
|
||||
<Avatar
|
||||
name={user?.first_name ? `${user?.first_name} ${user?.last_name ?? ""}` : user?.email}
|
||||
src={user?.avatar}
|
||||
size={24}
|
||||
shape="square"
|
||||
fallbackBackgroundColor="#FCBE1D"
|
||||
className="!text-base capitalize"
|
||||
/>
|
||||
)}
|
||||
<span className="text-sm font-medium text-custom-text-200">
|
||||
{user?.first_name ? `${user?.first_name} ${user?.last_name ?? ""}` : user?.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src={resolvedTheme === "dark" ? ProfileSetupDark : ProfileSetup}
|
||||
className="h-screen w-auto float-end object-cover"
|
||||
alt="Profile setup"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
export default OnBoardingPage;
|
@ -1,49 +0,0 @@
|
||||
// next imports
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Image from "next/image";
|
||||
// helpers
|
||||
import { EPageTypes } from "@/helpers/authentication.helper";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// wrappers
|
||||
import { AuthWrapper } from "@/lib/wrappers";
|
||||
// images
|
||||
import projectNotPublishedImage from "@/public/project-not-published.svg";
|
||||
|
||||
const CustomProjectNotPublishedError = observer(() => {
|
||||
// hooks
|
||||
const { instance } = useInstance();
|
||||
|
||||
const redirectionUrl = instance?.config?.app_base_url || "/";
|
||||
|
||||
return (
|
||||
<AuthWrapper pageType={EPageTypes.PUBLIC}>
|
||||
<div className="relative flex h-full min-h-screen w-screen items-center justify-center py-5">
|
||||
<div className="max-w-[700px] space-y-5">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<div className="relative h-[240px] w-[240px]">
|
||||
<Image src={projectNotPublishedImage} layout="fill" alt="404- Page not found" />
|
||||
</div>
|
||||
<div className="text-xl font-medium">
|
||||
Oops! The page you{`'`}re looking for isn{`'`}t live at the moment.
|
||||
</div>
|
||||
<div className="text-sm text-custom-text-200">
|
||||
If this is your project, login to your workspace to adjust its visibility settings and make it public.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center text-center">
|
||||
<a
|
||||
href={redirectionUrl}
|
||||
className="cursor-pointer select-none rounded-sm border border-gray-200 bg-gray-50 p-1.5 px-2.5 text-sm font-medium text-gray-700 transition-all hover:scale-105 hover:bg-gray-100 hover:text-gray-800"
|
||||
>
|
||||
Go to your Workspace
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
export default CustomProjectNotPublishedError;
|
BIN
space/public/instance/plane-takeoff.png
Normal file
BIN
space/public/instance/plane-takeoff.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
// store
|
||||
import { rootStore } from "@/lib/store-context";
|
||||
// import { rootStore } from "@/lib/store-context";
|
||||
|
||||
abstract class APIService {
|
||||
protected baseURL: string;
|
||||
@ -18,14 +18,14 @@ abstract class APIService {
|
||||
}
|
||||
|
||||
private setupInterceptors() {
|
||||
this.axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
const store = rootStore;
|
||||
if (error.response && error.response.status === 401 && store.user.data) store.user.reset();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
// this.axiosInstance.interceptors.response.use(
|
||||
// (response) => response,
|
||||
// (error) => {
|
||||
// const store = rootStore;
|
||||
// if (error.response && error.response.status === 401 && store.user.data) store.user.reset();
|
||||
// return Promise.reject(error);
|
||||
// }
|
||||
// );
|
||||
}
|
||||
|
||||
get(url: string, params = {}) {
|
||||
|
@ -18,15 +18,18 @@ type TError = {
|
||||
export interface IInstanceStore {
|
||||
// issues
|
||||
isLoading: boolean;
|
||||
instance: IInstance | undefined;
|
||||
data: IInstance | NonNullable<unknown>;
|
||||
config: Record<string, unknown>;
|
||||
error: TError | undefined;
|
||||
// action
|
||||
fetchInstanceInfo: () => Promise<void>;
|
||||
hydrate: (data: Record<string, unknown>, config: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
export class InstanceStore implements IInstanceStore {
|
||||
isLoading: boolean = true;
|
||||
instance: IInstance | undefined = undefined;
|
||||
data: IInstance | Record<string, unknown> = {};
|
||||
config: Record<string, unknown> = {};
|
||||
error: TError | undefined = undefined;
|
||||
// services
|
||||
instanceService;
|
||||
@ -35,15 +38,22 @@ export class InstanceStore implements IInstanceStore {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
isLoading: observable.ref,
|
||||
instance: observable,
|
||||
data: observable,
|
||||
config: observable,
|
||||
error: observable,
|
||||
// actions
|
||||
fetchInstanceInfo: action,
|
||||
hydrate: action,
|
||||
});
|
||||
// services
|
||||
this.instanceService = new InstanceService();
|
||||
}
|
||||
|
||||
hydrate = (data: Record<string, unknown>, config: Record<string, unknown>) => {
|
||||
this.data = { ...this.data, ...data };
|
||||
this.config = { ...this.config, ...config };
|
||||
};
|
||||
|
||||
/**
|
||||
* @description fetching instance information
|
||||
*/
|
||||
@ -51,10 +61,11 @@ export class InstanceStore implements IInstanceStore {
|
||||
try {
|
||||
this.isLoading = true;
|
||||
this.error = undefined;
|
||||
const instance = await this.instanceService.getInstanceInfo();
|
||||
const instanceDetails = await this.instanceService.getInstanceInfo();
|
||||
runInAction(() => {
|
||||
this.isLoading = false;
|
||||
this.instance = instance;
|
||||
this.data = instanceDetails.instance;
|
||||
this.config = instanceDetails.config;
|
||||
});
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
|
@ -1,52 +1,60 @@
|
||||
// mobx lite
|
||||
import { enableStaticRendering } from "mobx-react-lite";
|
||||
// store imports
|
||||
import { IInstanceStore, InstanceStore } from "@/store/instance.store";
|
||||
import { IProjectStore, ProjectStore } from "@/store/project";
|
||||
import { IUserStore, UserStore } from "@/store/user";
|
||||
import { IProfileStore, ProfileStore } from "@/store/user/profile.store";
|
||||
import { IUserStore, UserStore } from "@/store/user.store";
|
||||
|
||||
import IssueStore, { IIssueStore } from "./issue";
|
||||
import IssueDetailStore, { IIssueDetailStore } from "./issue_details";
|
||||
import { IIssuesFilterStore, IssuesFilterStore } from "./issues/issue-filters.store";
|
||||
import { IMentionsStore, MentionsStore } from "./mentions.store";
|
||||
// import { IProjectStore, ProjectStore } from "@/store/project";
|
||||
// import { IProfileStore, ProfileStore } from "@/store/user/profile.store";
|
||||
|
||||
// import IssueStore, { IIssueStore } from "./issue";
|
||||
// import IssueDetailStore, { IIssueDetailStore } from "./issue_details";
|
||||
// import { IIssuesFilterStore, IssuesFilterStore } from "./issues/issue-filters.store";
|
||||
// import { IMentionsStore, MentionsStore } from "./mentions.store";
|
||||
|
||||
enableStaticRendering(typeof window === "undefined");
|
||||
|
||||
export class RootStore {
|
||||
instance: IInstanceStore;
|
||||
user: IUserStore;
|
||||
profile: IProfileStore;
|
||||
project: IProjectStore;
|
||||
// profile: IProfileStore;
|
||||
// project: IProjectStore;
|
||||
|
||||
issue: IIssueStore;
|
||||
issueDetails: IIssueDetailStore;
|
||||
mentionsStore: IMentionsStore;
|
||||
issuesFilter: IIssuesFilterStore;
|
||||
// issue: IIssueStore;
|
||||
// issueDetails: IIssueDetailStore;
|
||||
// mentionsStore: IMentionsStore;
|
||||
// issuesFilter: IIssuesFilterStore;
|
||||
|
||||
constructor() {
|
||||
// makeObservable(this, {
|
||||
// instance: observable,
|
||||
// });
|
||||
this.instance = new InstanceStore(this);
|
||||
this.user = new UserStore(this);
|
||||
this.profile = new ProfileStore(this);
|
||||
this.project = new ProjectStore(this);
|
||||
// this.profile = new ProfileStore(this);
|
||||
// this.project = new ProjectStore(this);
|
||||
|
||||
this.issue = new IssueStore(this);
|
||||
this.issueDetails = new IssueDetailStore(this);
|
||||
this.mentionsStore = new MentionsStore(this);
|
||||
this.issuesFilter = new IssuesFilterStore(this);
|
||||
// this.issue = new IssueStore(this);
|
||||
// this.issueDetails = new IssueDetailStore(this);
|
||||
// this.mentionsStore = new MentionsStore(this);
|
||||
// this.issuesFilter = new IssuesFilterStore(this);
|
||||
}
|
||||
|
||||
resetOnSignOut = () => {
|
||||
localStorage.setItem("theme", "system");
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
hydrate = (data: any) => {
|
||||
if (!data) return;
|
||||
this.instance.hydrate(data?.instance || {}, data?.config || {});
|
||||
this.user.hydrate(data?.user || {});
|
||||
};
|
||||
|
||||
reset = () => {
|
||||
localStorage.setItem("theme", "system");
|
||||
this.instance = new InstanceStore(this);
|
||||
this.user = new UserStore(this);
|
||||
this.profile = new ProfileStore(this);
|
||||
this.project = new ProjectStore(this);
|
||||
|
||||
this.issue = new IssueStore(this);
|
||||
this.issueDetails = new IssueDetailStore(this);
|
||||
this.mentionsStore = new MentionsStore(this);
|
||||
this.issuesFilter = new IssuesFilterStore(this);
|
||||
// this.profile = new ProfileStore(this);
|
||||
// this.project = new ProjectStore(this);
|
||||
// this.issue = new IssueStore(this);
|
||||
// this.issueDetails = new IssueDetailStore(this);
|
||||
// this.mentionsStore = new MentionsStore(this);
|
||||
// this.issuesFilter = new IssuesFilterStore(this);
|
||||
};
|
||||
}
|
||||
|
183
space/store/user.store.ts
Normal file
183
space/store/user.store.ts
Normal file
@ -0,0 +1,183 @@
|
||||
import set from "lodash/set";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
// types
|
||||
import { IUser } from "@plane/types";
|
||||
// services
|
||||
import { AuthService } from "@/services/authentication.service";
|
||||
import { UserService } from "@/services/user.service";
|
||||
// stores
|
||||
import { RootStore } from "@/store/root.store";
|
||||
import { ProfileStore, IProfileStore } from "@/store/user/profile.store";
|
||||
import { ActorDetail } from "@/types/issue";
|
||||
|
||||
type TUserErrorStatus = {
|
||||
status: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export interface IUserStore {
|
||||
// observables
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
error: TUserErrorStatus | undefined;
|
||||
data: IUser | undefined;
|
||||
// store observables
|
||||
profile: IProfileStore;
|
||||
// computed
|
||||
currentActor: ActorDetail;
|
||||
// actions
|
||||
fetchCurrentUser: () => Promise<IUser | undefined>;
|
||||
updateCurrentUser: (data: Partial<IUser>) => Promise<IUser | undefined>;
|
||||
hydrate: (data: IUser) => void;
|
||||
reset: () => void;
|
||||
signOut: () => Promise<void>;
|
||||
}
|
||||
|
||||
export class UserStore implements IUserStore {
|
||||
// observables
|
||||
isAuthenticated: boolean = false;
|
||||
isLoading: boolean = false;
|
||||
error: TUserErrorStatus | undefined = undefined;
|
||||
data: IUser | undefined = undefined;
|
||||
// store observables
|
||||
profile: IProfileStore;
|
||||
// service
|
||||
userService: UserService;
|
||||
authService: AuthService;
|
||||
|
||||
constructor(private store: RootStore) {
|
||||
// stores
|
||||
this.profile = new ProfileStore(store);
|
||||
// service
|
||||
this.userService = new UserService();
|
||||
this.authService = new AuthService();
|
||||
// observables
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
isAuthenticated: observable.ref,
|
||||
isLoading: observable.ref,
|
||||
error: observable,
|
||||
// model observables
|
||||
data: observable,
|
||||
profile: observable,
|
||||
// computed
|
||||
currentActor: computed,
|
||||
// actions
|
||||
fetchCurrentUser: action,
|
||||
updateCurrentUser: action,
|
||||
reset: action,
|
||||
signOut: action,
|
||||
});
|
||||
}
|
||||
|
||||
// computed
|
||||
get currentActor(): ActorDetail {
|
||||
return {
|
||||
id: this.data?.id,
|
||||
first_name: this.data?.first_name,
|
||||
last_name: this.data?.last_name,
|
||||
display_name: this.data?.display_name,
|
||||
avatar: this.data?.avatar || undefined,
|
||||
is_bot: false,
|
||||
};
|
||||
}
|
||||
|
||||
// actions
|
||||
/**
|
||||
* @description fetches the current user
|
||||
* @returns {Promise<IUser>}
|
||||
*/
|
||||
fetchCurrentUser = async (): Promise<IUser> => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.isLoading = true;
|
||||
this.error = undefined;
|
||||
});
|
||||
const user = await this.userService.currentUser();
|
||||
if (user && user?.id) {
|
||||
await this.profile.fetchUserProfile();
|
||||
runInAction(() => {
|
||||
this.data = user;
|
||||
this.isLoading = false;
|
||||
this.isAuthenticated = true;
|
||||
});
|
||||
} else
|
||||
runInAction(() => {
|
||||
this.data = user;
|
||||
this.isLoading = false;
|
||||
this.isAuthenticated = false;
|
||||
});
|
||||
return user;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.isLoading = false;
|
||||
this.isAuthenticated = false;
|
||||
this.error = {
|
||||
status: "user-fetch-error",
|
||||
message: "Failed to fetch current user",
|
||||
};
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description updates the current user
|
||||
* @param data
|
||||
* @returns {Promise<IUser>}
|
||||
*/
|
||||
updateCurrentUser = async (data: Partial<IUser>): Promise<IUser> => {
|
||||
const currentUserData = this.data;
|
||||
try {
|
||||
if (currentUserData) {
|
||||
Object.keys(data).forEach((key: string) => {
|
||||
const userKey: keyof IUser = key as keyof IUser;
|
||||
if (this.data) set(this.data, userKey, data[userKey]);
|
||||
});
|
||||
}
|
||||
const user = await this.userService.updateUser(data);
|
||||
return user;
|
||||
} catch (error) {
|
||||
if (currentUserData) {
|
||||
Object.keys(currentUserData).forEach((key: string) => {
|
||||
const userKey: keyof IUser = key as keyof IUser;
|
||||
if (this.data) set(this.data, userKey, currentUserData[userKey]);
|
||||
});
|
||||
}
|
||||
runInAction(() => {
|
||||
this.error = {
|
||||
status: "user-update-error",
|
||||
message: "Failed to update current user",
|
||||
};
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
hydrate = (data: IUser): void => {
|
||||
this.data = { ...this.data, ...data };
|
||||
};
|
||||
|
||||
/**
|
||||
* @description resets the user store
|
||||
* @returns {void}
|
||||
*/
|
||||
reset = (): void => {
|
||||
runInAction(() => {
|
||||
this.isAuthenticated = false;
|
||||
this.isLoading = false;
|
||||
this.error = undefined;
|
||||
this.data = undefined;
|
||||
this.profile = new ProfileStore(this.store);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description signs out the current user
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
signOut = async (): Promise<void> => {
|
||||
// await this.authService.signOut(API_BASE_URL);
|
||||
// this.store.resetOnSignOut();
|
||||
};
|
||||
}
|
@ -1,12 +1,22 @@
|
||||
{
|
||||
"extends": "tsconfig/nextjs.json",
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "additional.d.ts"],
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "additional.d.ts", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"jsx": "preserve",
|
||||
"paths": {
|
||||
"@/*": ["*"]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user