diff --git a/packages/eslint-config-custom/index.js b/packages/eslint-config-custom/index.js index 9eae2e3f9..39d657d93 100644 --- a/packages/eslint-config-custom/index.js +++ b/packages/eslint-config-custom/index.js @@ -13,7 +13,7 @@ module.exports = { plugins: ["react", "@typescript-eslint", "import"], settings: { next: { - rootDir: ["web/", "space/", "packages/*/"], + rootDir: ["web/", "space/", "admin/", "packages/*/"], }, }, rules: { diff --git a/space/app/[workspace_slug]/[project_identifier]/layout.tsx b/space/app/[workspace_slug]/[project_identifier]/layout.tsx new file mode 100644 index 000000000..d831fd234 --- /dev/null +++ b/space/app/[workspace_slug]/[project_identifier]/layout.tsx @@ -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 }) => ( +
+
+ +
+
{children}
+ +
+ Plane logo +
+
+ Powered by Plane Deploy +
+
+
+); + +export default observer(ProjectLayout); diff --git a/space/pages/[workspace_slug]/[project_slug]/index.tsx b/space/app/[workspace_slug]/[project_identifier]/page.tsx similarity index 95% rename from space/pages/[workspace_slug]/[project_slug]/index.tsx rename to space/app/[workspace_slug]/[project_identifier]/page.tsx index aaec7672e..44ecd0aab 100644 --- a/space/pages/[workspace_slug]/[project_slug]/index.tsx +++ b/space/app/[workspace_slug]/[project_identifier]/page.tsx @@ -35,9 +35,6 @@ const WorkspaceProjectPage = (props: any) => { return ( - - {SITE_TITLE} - diff --git a/space/app/error.tsx b/space/app/error.tsx new file mode 100644 index 000000000..0c1e9d907 --- /dev/null +++ b/space/app/error.tsx @@ -0,0 +1,5 @@ +"use client"; + +export default function InstanceError() { + return
Instance Error
; +} diff --git a/space/app/layout.tsx b/space/app/layout.tsx new file mode 100644 index 000000000..24e23a372 --- /dev/null +++ b/space/app/layout.tsx @@ -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 ( + + + {/* + + + + */} + + + {!instanceDetails?.instance?.is_setup_done ? ( + + ) : ( + {children} + )} + + + ); +} diff --git a/space/app/page.tsx b/space/app/page.tsx new file mode 100644 index 000000000..ddf10a287 --- /dev/null +++ b/space/app/page.tsx @@ -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 ( + + + + ); +} diff --git a/space/components/instance/not-ready-view.tsx b/space/components/instance/not-ready-view.tsx index a28fcf3e7..5b94d92ed 100644 --- a/space/components/instance/not-ready-view.tsx +++ b/space/components/instance/not-ready-view.tsx @@ -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 ( -
-
-
-
-
- Plane logo -
-
- Instance not ready -
-
-

Your Plane instance isn{"'"}t ready yet

-

Ask your Instance Admin to complete set-up first.

- - - Get started - -
-
+
+
+
+

Welcome aboard Plane!

+ Plane Logo +

+ Get started by setting up your instance and workspace +

+
+
diff --git a/space/components/issues/navbar/index.tsx b/space/components/issues/navbar/index.tsx index de8633100..b63f86a44 100644 --- a/space/components/issues/navbar/index.tsx +++ b/space/components/issues/navbar/index.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useEffect } from "react"; import { observer } from "mobx-react-lite"; import Link from "next/link"; diff --git a/space/components/views/auth.tsx b/space/components/views/auth.tsx index cb36a6146..29dfa5680 100644 --- a/space/components/views/auth.tsx +++ b/space/components/views/auth.tsx @@ -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(() => {
) : ( <> - {currentUser ? ( + {isAuthenticated ? ( ) : (
diff --git a/space/helpers/common.helper.ts b/space/helpers/common.helper.ts index 085b34dc2..39f664ba4 100644 --- a/space/helpers/common.helper.ts +++ b/space/helpers/common.helper.ts @@ -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)); diff --git a/space/hooks/store/index.ts b/space/hooks/store/index.ts index 3b7ef07c9..9a48d10f2 100644 --- a/space/hooks/store/index.ts +++ b/space/hooks/store/index.ts @@ -1,4 +1,2 @@ -export * from "./user-mobx-provider"; - export * from "./use-instance"; export * from "./user"; diff --git a/space/hooks/store/use-instance.ts b/space/hooks/store/use-instance.ts index 92165e2bb..15fdaf84f 100644 --- a/space/hooks/store/use-instance.ts +++ b/space/hooks/store/use-instance.ts @@ -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 => { diff --git a/space/hooks/store/user-mobx-provider.ts b/space/hooks/store/user-mobx-provider.ts deleted file mode 100644 index 4fbc5591f..000000000 --- a/space/hooks/store/user-mobx-provider.ts +++ /dev/null @@ -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; -}; diff --git a/space/hooks/store/user/use-user-profile.ts b/space/hooks/store/user/use-user-profile.ts index d7f4c5569..61bfe64f8 100644 --- a/space/hooks/store/user/use-user-profile.ts +++ b/space/hooks/store/user/use-user-profile.ts @@ -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; }; diff --git a/space/hooks/store/user/use-user.ts b/space/hooks/store/user/use-user.ts index e491d88a2..c935946f8 100644 --- a/space/hooks/store/user/use-user.ts +++ b/space/hooks/store/user/use-user.ts @@ -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); diff --git a/space/lib/app-providers.tsx b/space/lib/app-providers.tsx new file mode 100644 index 000000000..389d68ab2 --- /dev/null +++ b/space/lib/app-providers.tsx @@ -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 ( + + {children} + + ); +}; diff --git a/space/lib/index.ts b/space/lib/index.ts deleted file mode 100644 index a10356821..000000000 --- a/space/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -export const init = {}; diff --git a/space/lib/store-context.tsx b/space/lib/store-context.tsx deleted file mode 100644 index 1eff1ddde..000000000 --- a/space/lib/store-context.tsx +++ /dev/null @@ -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); - -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 {children}; -}; diff --git a/space/lib/wrappers/auth-wrapper.tsx b/space/lib/wrappers/auth-wrapper.tsx index 1dad8f337..d976a5b80 100644 --- a/space/lib/wrappers/auth-wrapper.tsx +++ b/space/lib/wrappers/auth-wrapper.tsx @@ -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 diff --git a/space/package.json b/space/package.json index d27a23109..a10d190d2 100644 --- a/space/package.json +++ b/space/package.json @@ -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" }, diff --git a/space/pages/404.tsx b/space/pages/404.tsx deleted file mode 100644 index 4591f71f8..000000000 --- a/space/pages/404.tsx +++ /dev/null @@ -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 ( -
-
-
-
- 404- Page not found -
-
Oops! Something went wrong.
-
- Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is - temporarily unavailable. -
-
- - -
-
- ); -}); - -export default Custom404Error; diff --git a/space/pages/[workspace_slug]/index.tsx b/space/pages/[workspace_slug]/index.tsx deleted file mode 100644 index 635f3fdf9..000000000 --- a/space/pages/[workspace_slug]/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const WorkspaceProjectPage = () => ( -
Plane Workspace Space
-); - -export default WorkspaceProjectPage; diff --git a/space/pages/_app.tsx b/space/pages/_app.tsx deleted file mode 100644 index 363b61510..000000000 --- a/space/pages/_app.tsx +++ /dev/null @@ -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 ( - <> - - {SITE_TITLE} - - - - - - - - - - - - - - - - - - - - - - - - ); -} - -export default MyApp; diff --git a/space/pages/_document.tsx b/space/pages/_document.tsx deleted file mode 100644 index ae4455438..000000000 --- a/space/pages/_document.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import Document, { Html, Head, Main, NextScript } from "next/document"; - -class MyDocument extends Document { - render() { - return ( - - - -
-
- - - - ); - } -} - -export default MyDocument; diff --git a/space/pages/accounts/forgot-password.tsx b/space/pages/accounts/forgot-password.tsx deleted file mode 100644 index 494eae9d3..000000000 --- a/space/pages/accounts/forgot-password.tsx +++ /dev/null @@ -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({ - 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 ( - -
-
- Plane background pattern -
-
-
-
- Plane Logo - Plane -
-
-
-
-
-
-

- Reset your password -

-

- Enter your user account{"'"}s verified email address and we will send you a password reset link. -

-
-
-
- - checkEmailValidity(value) || "Email is invalid", - }} - render={({ field: { value, onChange, ref } }) => ( - 0} - /> - )} - /> - {resendTimerCode > 0 && ( -

- - We sent the reset link to your email address -

- )} -
- - - Back to sign in - -
-
-
-
-
-
-
- ); -}; - -export default ForgotPasswordPage; diff --git a/space/pages/accounts/reset-password.tsx b/space/pages/accounts/reset-password.tsx deleted file mode 100644 index 773acb10e..000000000 --- a/space/pages/accounts/reset-password.tsx +++ /dev/null @@ -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({ - ...defaultValues, - email: email ? email.toString() : "", - }); - const [csrfToken, setCsrfToken] = useState(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 ( - -
-
- Plane background pattern -
-
-
-
- Plane Logo - Plane -
-
-
-
-
-
-

- Set new password -

-

Secure your account with a strong password

-
-
- -
- -
- -
-
-
- -
- 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 ? ( - setShowPassword(false)} - /> - ) : ( - setShowPassword(true)} - /> - )} -
- {isPasswordInputFocused && } -
- {getPasswordStrength(resetFormData.password) >= 3 && ( -
- -
- 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 ? ( - setShowPassword(false)} - /> - ) : ( - setShowPassword(true)} - /> - )} -
- {!!resetFormData.confirm_password && - resetFormData.password !== resetFormData.confirm_password && ( - Passwords don{"'"}t match - )} -
- )} - -
-
-
-
-
-
-
- ); -}; - -export default ResetPasswordPage; diff --git a/space/pages/index.tsx b/space/pages/index.tsx deleted file mode 100644 index 8bba85cca..000000000 --- a/space/pages/index.tsx +++ /dev/null @@ -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(() => ( - - - -)); - -export default Index; diff --git a/space/pages/onboarding/index.tsx b/space/pages/onboarding/index.tsx deleted file mode 100644 index 8318f0346..000000000 --- a/space/pages/onboarding/index.tsx +++ /dev/null @@ -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 ( - -
-
-
-
-
- Plane Logo -
-
-
-
-
- {user?.avatar && ( - - )} - - {user?.first_name ? `${user?.first_name} ${user?.last_name ?? ""}` : user?.email} - -
-
-
-
-
-
-

Welcome to Plane!

-

- Let’s setup your profile, tell us a bit about yourself. -

-
- -
-
-
-
-
- {user?.avatar && ( - - )} - - {user?.first_name ? `${user?.first_name} ${user?.last_name ?? ""}` : user?.email} - -
-
-
- Profile setup -
-
-
-
- ); -}); - -export default OnBoardingPage; diff --git a/space/pages/project-not-published/index.tsx b/space/pages/project-not-published/index.tsx deleted file mode 100644 index 0bd25dd6e..000000000 --- a/space/pages/project-not-published/index.tsx +++ /dev/null @@ -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 ( - -
-
-
-
- 404- Page not found -
-
- Oops! The page you{`'`}re looking for isn{`'`}t live at the moment. -
-
- If this is your project, login to your workspace to adjust its visibility settings and make it public. -
-
- - -
-
-
- ); -}); - -export default CustomProjectNotPublishedError; diff --git a/space/public/instance/plane-takeoff.png b/space/public/instance/plane-takeoff.png new file mode 100644 index 000000000..417ff8299 Binary files /dev/null and b/space/public/instance/plane-takeoff.png differ diff --git a/space/services/api.service.ts b/space/services/api.service.ts index b6d353ccc..a5fe3e93d 100644 --- a/space/services/api.service.ts +++ b/space/services/api.service.ts @@ -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 = {}) { diff --git a/space/store/instance.store.ts b/space/store/instance.store.ts index db4fd87a9..c2499d15c 100644 --- a/space/store/instance.store.ts +++ b/space/store/instance.store.ts @@ -18,15 +18,18 @@ type TError = { export interface IInstanceStore { // issues isLoading: boolean; - instance: IInstance | undefined; + data: IInstance | NonNullable; + config: Record; error: TError | undefined; // action fetchInstanceInfo: () => Promise; + hydrate: (data: Record, config: Record) => void; } export class InstanceStore implements IInstanceStore { isLoading: boolean = true; - instance: IInstance | undefined = undefined; + data: IInstance | Record = {}; + config: Record = {}; 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, config: Record) => { + 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(() => { diff --git a/space/store/root.store.ts b/space/store/root.store.ts index fa0a25aaf..969292ea2 100644 --- a/space/store/root.store.ts +++ b/space/store/root.store.ts @@ -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); }; } diff --git a/space/store/user.store.ts b/space/store/user.store.ts new file mode 100644 index 000000000..7c37bc2c2 --- /dev/null +++ b/space/store/user.store.ts @@ -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; + updateCurrentUser: (data: Partial) => Promise; + hydrate: (data: IUser) => void; + reset: () => void; + signOut: () => Promise; +} + +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} + */ + fetchCurrentUser = async (): Promise => { + 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} + */ + updateCurrentUser = async (data: Partial): Promise => { + 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} + */ + signOut = async (): Promise => { + // await this.authService.signOut(API_BASE_URL); + // this.store.resetOnSignOut(); + }; +} diff --git a/space/tsconfig.json b/space/tsconfig.json index 9d3e164be..f7833dff1 100644 --- a/space/tsconfig.json +++ b/space/tsconfig.json @@ -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" + } + ] } }