diff --git a/packages/constants/src/auth.ts b/packages/constants/src/auth.ts index c12b63d63..065b188cf 100644 --- a/packages/constants/src/auth.ts +++ b/packages/constants/src/auth.ts @@ -1,5 +1,6 @@ import { ReactNode } from "react"; -import Link from "next/link"; +// import Link from "next/link"; +// import { EAuthenticationErrorCodes } from "@/helpers/authentication.helper"; export enum EPageTypes { PUBLIC = "PUBLIC", @@ -41,7 +42,7 @@ export enum EAuthErrorCodes { // Password strength INVALID_PASSWORD = "5020", // Sign Up - USER_ACCOUNT_DEACTIVATED = "5019", + // USER_ACCOUNT_DEACTIVATED = "5019", USER_ALREADY_EXIST = "5030", AUTHENTICATION_FAILED_SIGN_UP = "5035", REQUIRED_EMAIL_PASSWORD_SIGN_UP = "5040", @@ -91,281 +92,281 @@ export type TAuthErrorInfo = { message: ReactNode; }; -const errorCodeMessages: { - [key in EAuthErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode }; -} = { - // global - [EAuthErrorCodes.INSTANCE_NOT_CONFIGURED]: { - title: `Instance not configured`, - message: () => `Instance not configured. Please contact your administrator.`, - }, - [EAuthErrorCodes.SIGNUP_DISABLED]: { - title: `Sign up disabled`, - message: () => `Sign up disabled. Please contact your administrator.`, - }, - [EAuthErrorCodes.INVALID_PASSWORD]: { - title: `Invalid password`, - message: () => `Invalid password. Please try again.`, - }, - [EAuthErrorCodes.SMTP_NOT_CONFIGURED]: { - title: `SMTP not configured`, - message: () => `SMTP not configured. Please contact your administrator.`, - }, +// const errorCodeMessages: { +// [key in EAuthErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode }; +// } = { +// // global +// [EAuthErrorCodes.INSTANCE_NOT_CONFIGURED]: { +// title: `Instance not configured`, +// message: () => `Instance not configured. Please contact your administrator.`, +// }, +// [EAuthErrorCodes.SIGNUP_DISABLED]: { +// title: `Sign up disabled`, +// message: () => `Sign up disabled. Please contact your administrator.`, +// }, +// [EAuthErrorCodes.INVALID_PASSWORD]: { +// title: `Invalid password`, +// message: () => `Invalid password. Please try again.`, +// }, +// [EAuthErrorCodes.SMTP_NOT_CONFIGURED]: { +// title: `SMTP not configured`, +// message: () => `SMTP not configured. Please contact your administrator.`, +// }, // email check in both sign up and sign in - [EAuthErrorCodes.INVALID_EMAIL]: { - title: `Invalid email`, - message: () => `Invalid email. Please try again.`, - }, - [EAuthenticationErrorCodes.EMAIL_REQUIRED]: { - title: `Email required`, - message: () => `Email required. Please try again.`, - }, +// [EAuthErrorCodes.INVALID_EMAIL]: { +// title: `Invalid email`, +// message: () => `Invalid email. Please try again.`, +// }, +// [EAuthenticationErrorCodes.EMAIL_REQUIRED]: { +// title: `Email required`, +// message: () => `Email required. Please try again.`, +// }, - // sign up - [EAuthenticationErrorCodes.USER_ALREADY_EXIST]: { - title: `User already exists`, - message: (email = undefined) => ( -
- Your account is already registered.  - - Sign In - -  now. -
- ), - }, - [EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP]: { - title: `Email and password required`, - message: () => `Email and password required. Please try again.`, - }, - [EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP]: { - title: `Authentication failed`, - message: () => `Authentication failed. Please try again.`, - }, - [EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP]: { - title: `Invalid email`, - message: () => `Invalid email. Please try again.`, - }, - [EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED]: { - title: `Email and code required`, - message: () => `Email and code required. Please try again.`, - }, - [EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP]: { - title: `Invalid email`, - message: () => `Invalid email. Please try again.`, - }, +// // sign up +// [EAuthenticationErrorCodes.USER_ALREADY_EXIST]: { +// title: `User already exists`, +// message: (email = undefined) => ( +//
+// Your account is already registered.  +// +// Sign In +// +//  now. +//
+// ), +// }, +// [EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP]: { +// title: `Email and password required`, +// message: () => `Email and password required. Please try again.`, +// }, +// [EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP]: { +// title: `Authentication failed`, +// message: () => `Authentication failed. Please try again.`, +// }, +// [EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP]: { +// title: `Invalid email`, +// message: () => `Invalid email. Please try again.`, +// }, +// [EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED]: { +// title: `Email and code required`, +// message: () => `Email and code required. Please try again.`, +// }, +// [EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP]: { +// title: `Invalid email`, +// message: () => `Invalid email. Please try again.`, +// }, - // sign in - [EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED]: { - title: `User account deactivated`, - message: () =>
Your account is deactivated. Contact support@plane.so.
, - }, - [EAuthenticationErrorCodes.USER_DOES_NOT_EXIST]: { - title: `User does not exist`, - message: (email = undefined) => ( -
- No account found.  - - Create one - -  to get started. -
- ), - }, - [EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN]: { - title: `Email and password required`, - message: () => `Email and password required. Please try again.`, - }, - [EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN]: { - title: `Authentication failed`, - message: () => `Authentication failed. Please try again.`, - }, - [EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN]: { - title: `Invalid email`, - message: () => `Invalid email. Please try again.`, - }, - [EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED]: { - title: `Email and code required`, - message: () => `Email and code required. Please try again.`, - }, - [EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN]: { - title: `Invalid email`, - message: () => `Invalid email. Please try again.`, - }, +// // sign in +// [EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED]: { +// title: `User account deactivated`, +// message: () =>
Your account is deactivated. Contact support@plane.so.
, +// }, +// [EAuthenticationErrorCodes.USER_DOES_NOT_EXIST]: { +// title: `User does not exist`, +// message: (email = undefined) => ( +//
+// No account found.  +// +// Create one +// +//  to get started. +//
+// ), +// }, +// [EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN]: { +// title: `Email and password required`, +// message: () => `Email and password required. Please try again.`, +// }, +// [EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN]: { +// title: `Authentication failed`, +// message: () => `Authentication failed. Please try again.`, +// }, +// [EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN]: { +// title: `Invalid email`, +// message: () => `Invalid email. Please try again.`, +// }, +// [EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED]: { +// title: `Email and code required`, +// message: () => `Email and code required. Please try again.`, +// }, +// [EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN]: { +// title: `Invalid email`, +// message: () => `Invalid email. Please try again.`, +// }, - // Both Sign in and Sign up - [EAuthenticationErrorCodes.INVALID_MAGIC_CODE]: { - title: `Authentication failed`, - message: () => `Invalid magic code. Please try again.`, - }, - [EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE]: { - title: `Expired magic code`, - message: () => `Expired magic code. Please try again.`, - }, - [EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED]: { - title: `Expired magic code`, - message: () => `Expired magic code. Please try again.`, - }, +// // Both Sign in and Sign up +// [EAuthenticationErrorCodes.INVALID_MAGIC_CODE]: { +// title: `Authentication failed`, +// message: () => `Invalid magic code. Please try again.`, +// }, +// [EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE]: { +// title: `Expired magic code`, +// message: () => `Expired magic code. Please try again.`, +// }, +// [EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED]: { +// title: `Expired magic code`, +// message: () => `Expired magic code. Please try again.`, +// }, - // Oauth - [EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: { - title: `Google not configured`, - message: () => `Google not configured. Please contact your administrator.`, - }, - [EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED]: { - title: `GitHub not configured`, - message: () => `GitHub not configured. Please contact your administrator.`, - }, - [EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR]: { - title: `Google OAuth provider error`, - message: () => `Google OAuth provider error. Please try again.`, - }, - [EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR]: { - title: `GitHub OAuth provider error`, - message: () => `GitHub OAuth provider error. Please try again.`, - }, +// // Oauth +// [EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: { +// title: `Google not configured`, +// message: () => `Google not configured. Please contact your administrator.`, +// }, +// [EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED]: { +// title: `GitHub not configured`, +// message: () => `GitHub not configured. Please contact your administrator.`, +// }, +// [EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR]: { +// title: `Google OAuth provider error`, +// message: () => `Google OAuth provider error. Please try again.`, +// }, +// [EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR]: { +// title: `GitHub OAuth provider error`, +// message: () => `GitHub OAuth provider error. Please try again.`, +// }, - // Reset Password - [EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: { - title: `Invalid password token`, - message: () => `Invalid password token. Please try again.`, - }, - [EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN]: { - title: `Expired password token`, - message: () => `Expired password token. Please try again.`, - }, +// // Reset Password +// [EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: { +// title: `Invalid password token`, +// message: () => `Invalid password token. Please try again.`, +// }, +// [EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN]: { +// title: `Expired password token`, +// message: () => `Expired password token. Please try again.`, +// }, - // Change password +// // Change password - [EAuthenticationErrorCodes.MISSING_PASSWORD]: { - title: `Password required`, - message: () => `Password required. Please try again.`, - }, - [EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD]: { - title: `Incorrect old password`, - message: () => `Incorrect old password. Please try again.`, - }, - [EAuthenticationErrorCodes.INVALID_NEW_PASSWORD]: { - title: `Invalid new password`, - message: () => `Invalid new password. Please try again.`, - }, +// [EAuthenticationErrorCodes.MISSING_PASSWORD]: { +// title: `Password required`, +// message: () => `Password required. Please try again.`, +// }, +// [EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD]: { +// title: `Incorrect old password`, +// message: () => `Incorrect old password. Please try again.`, +// }, +// [EAuthenticationErrorCodes.INVALID_NEW_PASSWORD]: { +// title: `Invalid new password`, +// message: () => `Invalid new password. Please try again.`, +// }, - // set password - [EAuthenticationErrorCodes.PASSWORD_ALREADY_SET]: { - title: `Password already set`, - message: () => `Password already set. Please try again.`, - }, +// // set password +// [EAuthenticationErrorCodes.PASSWORD_ALREADY_SET]: { +// title: `Password already set`, +// message: () => `Password already set. Please try again.`, +// }, - // admin - [EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST]: { - title: `Admin already exists`, - message: () => `Admin already exists. Please try again.`, - }, - [EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: { - title: `Email, password and first name required`, - message: () => `Email, password and first name required. Please try again.`, - }, - [EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL]: { - title: `Invalid admin email`, - message: () => `Invalid admin email. Please try again.`, - }, - [EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD]: { - title: `Invalid admin password`, - message: () => `Invalid admin password. Please try again.`, - }, - [EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: { - title: `Email and password required`, - message: () => `Email and password required. Please try again.`, - }, - [EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED]: { - title: `Authentication failed`, - message: () => `Authentication failed. Please try again.`, - }, - [EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST]: { - title: `Admin user already exists`, - message: () => ( -
- Admin user already exists.  - - Sign In - -  now. -
- ), - }, - [EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: { - title: `Admin user does not exist`, - message: () => ( -
- Admin user does not exist.  - - Sign In - -  now. -
- ), - }, -}; +// // admin +// [EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST]: { +// title: `Admin already exists`, +// message: () => `Admin already exists. Please try again.`, +// }, +// [EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: { +// title: `Email, password and first name required`, +// message: () => `Email, password and first name required. Please try again.`, +// }, +// [EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL]: { +// title: `Invalid admin email`, +// message: () => `Invalid admin email. Please try again.`, +// }, +// [EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD]: { +// title: `Invalid admin password`, +// message: () => `Invalid admin password. Please try again.`, +// }, +// [EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: { +// title: `Email and password required`, +// message: () => `Email and password required. Please try again.`, +// }, +// [EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED]: { +// title: `Authentication failed`, +// message: () => `Authentication failed. Please try again.`, +// }, +// [EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST]: { +// title: `Admin user already exists`, +// message: () => ( +//
+// Admin user already exists.  +// +// Sign In +// +//  now. +//
+// ), +// }, +// [EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: { +// title: `Admin user does not exist`, +// message: () => ( +//
+// Admin user does not exist.  +// +// Sign In +// +//  now. +//
+// ), +// }, +// }; -export const authErrorHandler = ( - errorCode: EAuthenticationErrorCodes, - email?: string | undefined -): TAuthErrorInfo | undefined => { - const bannerAlertErrorCodes = [ - EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED, - EAuthenticationErrorCodes.INVALID_EMAIL, - EAuthenticationErrorCodes.EMAIL_REQUIRED, - EAuthenticationErrorCodes.SIGNUP_DISABLED, - EAuthenticationErrorCodes.INVALID_PASSWORD, - EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED, - EAuthenticationErrorCodes.USER_ALREADY_EXIST, - EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP, - EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP, - EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP, - EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP, - EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED, - EAuthenticationErrorCodes.USER_DOES_NOT_EXIST, - EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN, - EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN, - EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN, - EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN, - EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED, - EAuthenticationErrorCodes.INVALID_MAGIC_CODE, - EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE, - EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED, - EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED, - EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED, - EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR, - EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR, - EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN, - EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN, - EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD, - EAuthenticationErrorCodes.INVALID_NEW_PASSWORD, - EAuthenticationErrorCodes.PASSWORD_ALREADY_SET, - EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST, - EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME, - EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL, - EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD, - EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD, - EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED, - EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST, - EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST, - ]; +// export const authErrorHandler = ( +// errorCode: EAuthenticationErrorCodes, +// email?: string | undefined +// ): TAuthErrorInfo | undefined => { +// const bannerAlertErrorCodes = [ +// EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED, +// EAuthenticationErrorCodes.INVALID_EMAIL, +// EAuthenticationErrorCodes.EMAIL_REQUIRED, +// EAuthenticationErrorCodes.SIGNUP_DISABLED, +// EAuthenticationErrorCodes.INVALID_PASSWORD, +// EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED, +// EAuthenticationErrorCodes.USER_ALREADY_EXIST, +// EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP, +// EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP, +// EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP, +// EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP, +// EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED, +// EAuthenticationErrorCodes.USER_DOES_NOT_EXIST, +// EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN, +// EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN, +// EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN, +// EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN, +// EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED, +// // EAuthenticationErrorCodes.INVALID_MAGIC_CODE, +// // EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE, +// // EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED, +// EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED, +// EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED, +// EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR, +// EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR, +// EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN, +// EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN, +// EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD, +// EAuthenticationErrorCodes.INVALID_NEW_PASSWORD, +// EAuthenticationErrorCodes.PASSWORD_ALREADY_SET, +// EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST, +// EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME, +// EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL, +// EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD, +// EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD, +// EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED, +// EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST, +// EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST, +// ]; - if (bannerAlertErrorCodes.includes(errorCode)) - return { - type: EErrorAlertType.BANNER_ALERT, - code: errorCode, - title: errorCodeMessages[errorCode]?.title || "Error", - message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.", - }; +// if (bannerAlertErrorCodes.includes(errorCode)) +// return { +// type: EErrorAlertType.BANNER_ALERT, +// code: errorCode, +// title: errorCodeMessages[errorCode]?.title || "Error", +// message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.", +// }; - return undefined; -}; +// return undefined; +// }; diff --git a/packages/tailwind-config-custom/tailwind.config.js b/packages/tailwind-config-custom/tailwind.config.js index 42e176043..9fca24b0a 100644 --- a/packages/tailwind-config-custom/tailwind.config.js +++ b/packages/tailwind-config-custom/tailwind.config.js @@ -8,6 +8,7 @@ module.exports = { content: { relative: true, files: [ + "./app/**/*.{js,ts,jsx,tsx}", "./components/**/*.tsx", "./constants/**/*.{js,ts,jsx,tsx}", "./layouts/**/*.tsx", diff --git a/packages/ui/src/sortable/sortable.stories.tsx b/packages/ui/src/sortable/sortable.stories.tsx index 2d469b767..6d40ddc2e 100644 --- a/packages/ui/src/sortable/sortable.stories.tsx +++ b/packages/ui/src/sortable/sortable.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import React from "react"; +import { Draggable } from "./draggable"; import { Sortable } from "./sortable"; const meta: Meta = { @@ -12,7 +13,7 @@ type Story = StoryObj; const data = [ { id: "1", name: "John Doe" }, - { id: "2", name: "Satish" }, + { id: "2", name: "Jane Doe 2" }, { id: "3", name: "Alice" }, { id: "4", name: "Bob" }, { id: "5", name: "Charlie" }, diff --git a/space/types/project.d.ts b/space/types/project.d.ts new file mode 100644 index 000000000..90c89ed80 --- /dev/null +++ b/space/types/project.d.ts @@ -0,0 +1,42 @@ +import { TLogoProps } from "@plane/types"; + +export type TWorkspaceDetails = { + name: string; + slug: string; + id: string; +}; + +export type TViewDetails = { + list: boolean; + gantt: boolean; + kanban: boolean; + calendar: boolean; + spreadsheet: boolean; +}; + +export type TProjectDetails = { + id: string; + identifier: string; + name: string; + cover_image: string | undefined; + logo_props: TLogoProps; + description: string; +}; + +export type TProjectSettings = { + id: string; + anchor: string; + comments: boolean; + reactions: boolean; + votes: boolean; + inbox: unknown; + workspace: string; + workspace_detail: TWorkspaceDetails; + project: string; + project_details: TProjectDetails; + views: TViewDetails; + created_by: string; + updated_by: string; + created_at: string; + updated_at: string; +}; diff --git a/web/components/headers/workspace-active-cycles.tsx b/web/app/[workspaceSlug]/@header/active-cycles/header.tsx similarity index 89% rename from web/components/headers/workspace-active-cycles.tsx rename to web/app/[workspaceSlug]/@header/active-cycles/header.tsx index 5861cba60..eb6e8633d 100644 --- a/web/components/headers/workspace-active-cycles.tsx +++ b/web/app/[workspaceSlug]/@header/active-cycles/header.tsx @@ -1,3 +1,5 @@ +"use client"; + import { observer } from "mobx-react"; // ui import { Crown } from "lucide-react"; @@ -5,7 +7,7 @@ import { Breadcrumbs, ContrastIcon } from "@plane/ui"; import { BreadcrumbLink } from "@/components/common"; // icons -export const WorkspaceActiveCycleHeader = observer(() => ( +const WorkspaceActiveCycleHeader = observer(() => (
@@ -25,3 +27,5 @@ export const WorkspaceActiveCycleHeader = observer(() => (
)); + +export default WorkspaceActiveCycleHeader; diff --git a/web/app/[workspaceSlug]/@header/active-cycles/page.tsx b/web/app/[workspaceSlug]/@header/active-cycles/page.tsx new file mode 100644 index 000000000..56083aab7 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/active-cycles/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +// components +import AppHeaderWrapper from "../../app-header-wrapper"; +import WorkspaceActiveCycleHeader from "./header"; + +const WorkspaceActiveCycleHeaderPage = () => } />; + +export default WorkspaceActiveCycleHeaderPage; diff --git a/web/components/headers/workspace-analytics.tsx b/web/app/[workspaceSlug]/@header/analytics/header.tsx similarity index 89% rename from web/components/headers/workspace-analytics.tsx rename to web/app/[workspaceSlug]/@header/analytics/header.tsx index 98ceccbca..51254da58 100644 --- a/web/components/headers/workspace-analytics.tsx +++ b/web/app/[workspaceSlug]/@header/analytics/header.tsx @@ -1,17 +1,22 @@ +"use client"; + import { useEffect } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useSearchParams } from "next/navigation"; +// icons import { BarChart2, PanelRight } from "lucide-react"; // ui import { Breadcrumbs } from "@plane/ui"; // components import { BreadcrumbLink } from "@/components/common"; +// helpers import { cn } from "@/helpers/common.helper"; +// hooks import { useAppTheme } from "@/hooks/store"; -export const WorkspaceAnalyticsHeader = observer(() => { - const router = useRouter(); - const { analytics_tab } = router.query; +const WorkspaceAnalyticsHeader = observer(() => { + const searchParams = useSearchParams(); + const analytics_tab = searchParams.get("analytics_tab"); // store hooks const { workspaceAnalyticsSidebarCollapsed, toggleWorkspaceAnalyticsSidebar } = useAppTheme(); @@ -66,3 +71,5 @@ export const WorkspaceAnalyticsHeader = observer(() => { ); }); + +export default WorkspaceAnalyticsHeader; diff --git a/web/app/[workspaceSlug]/@header/analytics/page.tsx b/web/app/[workspaceSlug]/@header/analytics/page.tsx new file mode 100644 index 000000000..405057b05 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/analytics/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +// components +import AppHeaderWrapper from "../../app-header-wrapper"; +import WorkspaceAnalyticsHeader from "./header"; + +const WorkspaceAnalyticsHeaderPage = () => } />; + +export default WorkspaceAnalyticsHeaderPage; diff --git a/web/components/headers/workspace-dashboard.tsx b/web/app/[workspaceSlug]/@header/header.tsx similarity index 96% rename from web/components/headers/workspace-dashboard.tsx rename to web/app/[workspaceSlug]/@header/header.tsx index 880b44406..4afd12399 100644 --- a/web/components/headers/workspace-dashboard.tsx +++ b/web/app/[workspaceSlug]/@header/header.tsx @@ -1,18 +1,21 @@ +"use client"; + import Image from "next/image"; import { useTheme } from "next-themes"; import { Home, Zap } from "lucide-react"; // images import githubBlackImage from "/public/logos/github-black.png"; import githubWhiteImage from "/public/logos/github-white.png"; -// hooks -// components +// ui import { Breadcrumbs } from "@plane/ui"; +// components import { BreadcrumbLink } from "@/components/common"; // constants import { CHANGELOG_REDIRECTED, GITHUB_REDIRECTED } from "@/constants/event-tracker"; +// hooks import { useEventTracker } from "@/hooks/store"; -export const WorkspaceDashboardHeader = () => { +const WorkspaceDashboardHeader = () => { // hooks const { captureEvent } = useEventTracker(); const { resolvedTheme } = useTheme(); @@ -69,3 +72,5 @@ export const WorkspaceDashboardHeader = () => { ); }; + +export default WorkspaceDashboardHeader; diff --git a/web/app/[workspaceSlug]/@header/page.tsx b/web/app/[workspaceSlug]/@header/page.tsx new file mode 100644 index 000000000..4175c5f38 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +// components +import AppHeaderWrapper from "../app-header-wrapper"; +import WorkspaceDashboardHeader from "./header"; + +const WorkspaceDashboardHeaderPage = () => } />; + +export default WorkspaceDashboardHeaderPage; diff --git a/web/app/[workspaceSlug]/@header/profile/[userId]/activity/page.tsx b/web/app/[workspaceSlug]/@header/profile/[userId]/activity/page.tsx new file mode 100644 index 000000000..56d3a1468 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/profile/[userId]/activity/page.tsx @@ -0,0 +1,8 @@ +"use client"; + +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import UserProfileHeader from "../header"; + +const ProfileActivityHeader = () => } />; + +export default ProfileActivityHeader; diff --git a/web/app/[workspaceSlug]/@header/profile/[userId]/assigned/page.tsx b/web/app/[workspaceSlug]/@header/profile/[userId]/assigned/page.tsx new file mode 100644 index 000000000..a6253fca3 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/profile/[userId]/assigned/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import UserProfileHeader from "../header"; +import ProfileIssuesMobileHeader from "../mobile-header"; + +const ProfileAssignedHeader = () => ( + } mobileHeader={} /> +); + +export default ProfileAssignedHeader; diff --git a/web/app/[workspaceSlug]/@header/profile/[userId]/created/page.tsx b/web/app/[workspaceSlug]/@header/profile/[userId]/created/page.tsx new file mode 100644 index 000000000..e46ef7b25 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/profile/[userId]/created/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import UserProfileHeader from "../header"; +import ProfileIssuesMobileHeader from "../mobile-header"; + +const ProfileCreatedHeader = () => ( + } mobileHeader={} /> +); + +export default ProfileCreatedHeader; diff --git a/web/components/headers/user-profile.tsx b/web/app/[workspaceSlug]/@header/profile/[userId]/header.tsx similarity index 93% rename from web/components/headers/user-profile.tsx rename to web/app/[workspaceSlug]/@header/profile/[userId]/header.tsx index 4f1f44659..1f15e2c48 100644 --- a/web/components/headers/user-profile.tsx +++ b/web/app/[workspaceSlug]/@header/profile/[userId]/header.tsx @@ -1,8 +1,10 @@ +"use client"; + // ui import { FC } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import { ChevronDown, PanelRight } from "lucide-react"; import { Breadcrumbs, CustomMenu } from "@plane/ui"; import { BreadcrumbLink } from "@/components/common"; @@ -15,11 +17,10 @@ type TUserProfileHeader = { type?: string | undefined; }; -export const UserProfileHeader: FC = observer((props) => { +const UserProfileHeader: FC = observer((props) => { const { type = undefined } = props; // router - const router = useRouter(); - const { workspaceSlug, userId } = router.query; + const { workspaceSlug, userId } = useParams(); // store hooks const { toggleProfileSidebar, profileSidebarCollapsed } = useAppTheme(); const { @@ -89,3 +90,5 @@ export const UserProfileHeader: FC = observer((props) => {
); }); + +export default UserProfileHeader; diff --git a/web/components/profile/profile-issues-mobile-header.tsx b/web/app/[workspaceSlug]/@header/profile/[userId]/mobile-header.tsx similarity index 98% rename from web/components/profile/profile-issues-mobile-header.tsx rename to web/app/[workspaceSlug]/@header/profile/[userId]/mobile-header.tsx index 6088d0194..99f6e0c9c 100644 --- a/web/components/profile/profile-issues-mobile-header.tsx +++ b/web/app/[workspaceSlug]/@header/profile/[userId]/mobile-header.tsx @@ -1,6 +1,8 @@ +"use client"; + import { useCallback } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; // icons import { ChevronDown } from "lucide-react"; // types @@ -18,8 +20,7 @@ import { useIssues, useLabel } from "@/hooks/store"; const ProfileIssuesMobileHeader = observer(() => { // router - const router = useRouter(); - const { workspaceSlug, userId } = router.query; + const { workspaceSlug, userId } = useParams(); // store hook const { issuesFilter: { issueFilters, updateFilters }, diff --git a/web/app/[workspaceSlug]/@header/profile/[userId]/page.tsx b/web/app/[workspaceSlug]/@header/profile/[userId]/page.tsx new file mode 100644 index 000000000..df2b0d83e --- /dev/null +++ b/web/app/[workspaceSlug]/@header/profile/[userId]/page.tsx @@ -0,0 +1,8 @@ +"use client"; + +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import UserProfileHeader from "./header"; + +const ProfileOverviewHeader = () => } />; + +export default ProfileOverviewHeader; diff --git a/web/app/[workspaceSlug]/@header/profile/[userId]/subscribed/page.tsx b/web/app/[workspaceSlug]/@header/profile/[userId]/subscribed/page.tsx new file mode 100644 index 000000000..75caa98c9 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/profile/[userId]/subscribed/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import UserProfileHeader from "../header"; +import ProfileIssuesMobileHeader from "../mobile-header"; + +const ProfileSubscribedHeader = () => ( + } mobileHeader={} /> +); + +export default ProfileSubscribedHeader; diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/archives/[...default-header]/page.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/archives/[...default-header]/page.tsx new file mode 100644 index 000000000..6e5cd0160 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/archives/[...default-header]/page.tsx @@ -0,0 +1,2 @@ +import DefaultProjectArchivesHeader from "../page"; +export default DefaultProjectArchivesHeader; \ No newline at end of file diff --git a/web/components/headers/project-archives.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/archives/header.tsx similarity index 92% rename from web/components/headers/project-archives.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/archives/header.tsx index 502241461..aba65352e 100644 --- a/web/components/headers/project-archives.tsx +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/archives/header.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; +import { useParams, usePathname, useRouter } from "next/navigation"; // ui import { ArchiveIcon, Breadcrumbs, Tooltip } from "@plane/ui"; // components @@ -12,11 +12,12 @@ import { EIssuesStoreType } from "@/constants/issue"; import { useIssues, useProject } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; -export const ProjectArchivesHeader: FC = observer(() => { +const ProjectArchivesHeader: FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - const activeTab = router.pathname.split("/").pop(); + const { workspaceSlug, projectId } = useParams(); + const pathname = usePathname(); + const activeTab = pathname.split("/").pop(); // store hooks const { issuesFilter: { issueFilters }, @@ -93,3 +94,5 @@ export const ProjectArchivesHeader: FC = observer(() => { ); }); + +export default ProjectArchivesHeader; diff --git a/web/components/headers/project-archived-issue-details.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/archives/issues/[archivedIssueId]/header.tsx similarity index 92% rename from web/components/headers/project-archived-issue-details.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/archives/issues/[archivedIssueId]/header.tsx index c874745a4..75b8dce43 100644 --- a/web/components/headers/project-archived-issue-details.tsx +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/archives/issues/[archivedIssueId]/header.tsx @@ -1,27 +1,23 @@ import { FC } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import useSWR from "swr"; -// hooks -import { ArchiveIcon, Breadcrumbs, LayersIcon } from "@plane/ui"; -import { BreadcrumbLink, Logo } from "@/components/common"; -import { ISSUE_DETAILS } from "@/constants/fetch-keys"; -import { useProject } from "@/hooks/store"; -// components // ui -// types -import { IssueArchiveService } from "@/services/issue"; -// constants -// services -// helpers +import { ArchiveIcon, Breadcrumbs, LayersIcon } from "@plane/ui"; // components +import { BreadcrumbLink, Logo } from "@/components/common"; +// constants +import { ISSUE_DETAILS } from "@/constants/fetch-keys"; +// hooks +import { useProject } from "@/hooks/store"; +// services +import { IssueArchiveService } from "@/services/issue"; const issueArchiveService = new IssueArchiveService(); -export const ProjectArchivedIssueDetailsHeader: FC = observer(() => { +const ProjectArchivedIssueDetailsHeader: FC = observer(() => { // router - const router = useRouter(); - const { workspaceSlug, projectId, archivedIssueId } = router.query; + const { workspaceSlug, projectId, archivedIssueId } = useParams(); // store hooks const { currentProjectDetails } = useProject(); @@ -96,3 +92,5 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => { ); }); + +export default ProjectArchivedIssueDetailsHeader; diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/archives/issues/[archivedIssueId]/page.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/archives/issues/[archivedIssueId]/page.tsx new file mode 100644 index 000000000..60fa754ce --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/archives/issues/[archivedIssueId]/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import ProjectArchivedIssueDetailsHeader from "./header"; + +const ProjectArchivedIssueDetailsHeaderPage = () => } />; + +export default ProjectArchivedIssueDetailsHeaderPage; diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/archives/page.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/archives/page.tsx new file mode 100644 index 000000000..aa1b03169 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/archives/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import ProjectArchivesHeader from "./header"; + +const ProjectArchivesHeaderPage = () => } />; + +export default ProjectArchivesHeaderPage; diff --git a/web/components/headers/cycle-issues.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/cycles/[cycleId]/header.tsx similarity index 96% rename from web/components/headers/cycle-issues.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/cycles/[cycleId]/header.tsx index 4e9c37c9d..3f70a647d 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/cycles/[cycleId]/header.tsx @@ -1,7 +1,7 @@ import { useCallback, useState } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -import { useRouter } from "next/router"; +import { useParams, useRouter } from "next/navigation"; // icons import { ArrowRight, PanelRight } from "lucide-react"; // types @@ -36,8 +36,7 @@ import { usePlatformOS } from "@/hooks/use-platform-os"; const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => { // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId } = useParams(); // store hooks const { getCycleById } = useCycle(); // derived values @@ -56,12 +55,12 @@ const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => { ); }; -export const CycleIssuesHeader: React.FC = observer(() => { +const CycleIssuesHeader: React.FC = observer(() => { // states const [analyticsModal, setAnalyticsModal] = useState(false); // router const router = useRouter(); - const { workspaceSlug, projectId, cycleId } = router.query as { + const { workspaceSlug, projectId, cycleId } = useParams() as { workspaceSlug: string; projectId: string; cycleId: string; @@ -206,9 +205,8 @@ export const CycleIssuesHeader: React.FC = observer(() => { {issuesCount && issuesCount > 0 ? ( 1 ? "issues" : "issue" - } in this cycle`} + tooltipContent={`There are ${issuesCount} ${issuesCount > 1 ? "issues" : "issue" + } in this cycle`} position="bottom" > @@ -303,3 +301,5 @@ export const CycleIssuesHeader: React.FC = observer(() => { ); }); + +export default CycleIssuesHeader; diff --git a/web/components/cycles/cycle-mobile-header.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/cycles/[cycleId]/mobile-header.tsx similarity index 97% rename from web/components/cycles/cycle-mobile-header.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/cycles/[cycleId]/mobile-header.tsx index ce1b45a9b..e68829774 100644 --- a/web/components/cycles/cycle-mobile-header.tsx +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/cycles/[cycleId]/mobile-header.tsx @@ -1,5 +1,5 @@ import { useCallback, useState } from "react"; -import router from "next/router"; +import { useParams } from "next/navigation"; // icons import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; // types @@ -16,7 +16,7 @@ import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useIssues, useCycle, useProjectState, useLabel, useMember, useProject } from "@/hooks/store"; -export const CycleMobileHeader = () => { +const CycleIssuesMobileHeader = () => { const [analyticsModal, setAnalyticsModal] = useState(false); const { getCycleById } = useCycle(); const layouts = [ @@ -25,7 +25,7 @@ export const CycleMobileHeader = () => { { key: "calendar", title: "Calendar", icon: Calendar }, ]; - const { workspaceSlug, projectId, cycleId } = router.query; + const { workspaceSlug, projectId, cycleId } = useParams(); const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; // store hooks const { currentProjectDetails } = useProject(); @@ -202,3 +202,5 @@ export const CycleMobileHeader = () => { ); }; + +export default CycleIssuesMobileHeader; diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/cycles/[cycleId]/page.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/cycles/[cycleId]/page.tsx new file mode 100644 index 000000000..6afb97002 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/cycles/[cycleId]/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import CycleIssuesHeader from "./header"; +import CycleIssuesMobileHeader from "./mobile-header"; + +const CycleIssuesHeaderPage = () => ( + } mobileHeader={} /> +); + +export default CycleIssuesHeaderPage; diff --git a/web/components/headers/cycles.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/cycles/header.tsx similarity index 93% rename from web/components/headers/cycles.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/cycles/header.tsx index 76493bd51..1a0993d80 100644 --- a/web/components/headers/cycles.tsx +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/cycles/header.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; +import { useParams, useRouter } from "next/navigation"; // ui import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui"; // components @@ -11,10 +11,10 @@ import { EUserProjectRoles } from "@/constants/project"; // hooks import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store"; -export const CyclesHeader: FC = observer(() => { +const CyclesHeader: FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug } = useParams(); // store hooks const { toggleCreateCycleModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); @@ -72,3 +72,5 @@ export const CyclesHeader: FC = observer(() => { ); }); + +export default CyclesHeader; diff --git a/web/components/cycles/cycles-list-mobile-header.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/cycles/mobile-header.tsx similarity index 100% rename from web/components/cycles/cycles-list-mobile-header.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/cycles/mobile-header.tsx diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/cycles/page.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/cycles/page.tsx new file mode 100644 index 000000000..457e9b3dc --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/cycles/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import CyclesHeader from "./header"; +import CyclesListMobileHeader from "./mobile-header"; + +const CyclesHeaderPage = () => ( + } mobileHeader={} /> +); + +export default CyclesHeaderPage; diff --git a/web/components/headers/project-draft-issues.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/draft-issues/header.tsx similarity index 96% rename from web/components/headers/project-draft-issues.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/draft-issues/header.tsx index f3e70d86b..dc2a393bf 100644 --- a/web/components/headers/project-draft-issues.tsx +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/draft-issues/header.tsx @@ -1,6 +1,6 @@ import { FC, useCallback } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // ui @@ -16,10 +16,9 @@ import { calculateTotalFilters } from "@/helpers/filter.helper"; import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; -export const ProjectDraftIssueHeader: FC = observer(() => { +const ProjectDraftIssueHeader: FC = observer(() => { // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string }; // store hooks const { issuesFilter: { issueFilters, updateFilters }, @@ -168,3 +167,5 @@ export const ProjectDraftIssueHeader: FC = observer(() => { ); }); + +export default ProjectDraftIssueHeader; diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/draft-issues/page.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/draft-issues/page.tsx new file mode 100644 index 000000000..a31440f48 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/draft-issues/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import ProjectDraftIssueHeader from "./header"; + +const ProjectDraftIssueHeaderPage = () => } />; + +export default ProjectDraftIssueHeaderPage; diff --git a/web/components/headers/project-inbox.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/inbox/header.tsx similarity index 93% rename from web/components/headers/project-inbox.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/inbox/header.tsx index ce76f3e40..b7fd70295 100644 --- a/web/components/headers/project-inbox.tsx +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/inbox/header.tsx @@ -1,6 +1,6 @@ import { FC, useState } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import { RefreshCcw } from "lucide-react"; // ui import { Breadcrumbs, Button, LayersIcon } from "@plane/ui"; @@ -10,12 +10,11 @@ import { InboxIssueCreateEditModalRoot } from "@/components/inbox"; // hooks import { useProject, useProjectInbox } from "@/hooks/store"; -export const ProjectInboxHeader: FC = observer(() => { +const ProjectInboxHeader: FC = observer(() => { // states const [createIssueModal, setCreateIssueModal] = useState(false); // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId } = useParams(); // store hooks const { currentProjectDetails } = useProject(); const { loader } = useProjectInbox(); @@ -77,3 +76,5 @@ export const ProjectInboxHeader: FC = observer(() => { ); }); + +export default ProjectInboxHeader; diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/inbox/page.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/inbox/page.tsx new file mode 100644 index 000000000..90d616606 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/inbox/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import ProjectInboxHeader from "./header"; + +const ProjectInboxHeaderPage = () => } />; + +export default ProjectInboxHeaderPage; diff --git a/web/components/headers/project-issue-details.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/issues/[issueId]/header.tsx similarity index 92% rename from web/components/headers/project-issue-details.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/issues/[issueId]/header.tsx index 890bd59e5..f8966e001 100644 --- a/web/components/headers/project-issue-details.tsx +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/issues/[issueId]/header.tsx @@ -1,22 +1,20 @@ import { FC } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; -// hooks +import { useParams, useRouter } from "next/navigation"; import { PanelRight } from "lucide-react"; -import { Breadcrumbs, LayersIcon } from "@plane/ui"; -import { BreadcrumbLink, Logo } from "@/components/common"; -import { cn } from "@/helpers/common.helper"; -import { useAppTheme, useIssueDetail, useProject } from "@/hooks/store"; // ui -// helpers -// services -// constants +import { Breadcrumbs, LayersIcon } from "@plane/ui"; // components +import { BreadcrumbLink, Logo } from "@/components/common"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useAppTheme, useIssueDetail, useProject } from "@/hooks/store"; -export const ProjectIssueDetailsHeader: FC = observer(() => { +const ProjectIssueDetailsHeader: FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; + const { workspaceSlug, projectId, issueId } = useParams(); // store hooks const { currentProjectDetails } = useProject(); const { issueDetailSidebarCollapsed, toggleIssueDetailSidebar } = useAppTheme(); @@ -83,3 +81,5 @@ export const ProjectIssueDetailsHeader: FC = observer(() => { ); }); + +export default ProjectIssueDetailsHeader; diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/issues/[issueId]/page.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/issues/[issueId]/page.tsx new file mode 100644 index 000000000..1c64134f0 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/issues/[issueId]/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import ProjectIssueDetailsHeader from "./header"; + +const ProjectIssueDetailsHeaderPage = () => } />; + +export default ProjectIssueDetailsHeaderPage; diff --git a/web/components/headers/project-issues.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/issues/header.tsx similarity index 97% rename from web/components/headers/project-issues.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/issues/header.tsx index e3983d39c..44bebde89 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/issues/header.tsx @@ -1,6 +1,6 @@ import { useCallback, useState } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams, useRouter } from "next/navigation"; // icons import { Briefcase, Circle, ExternalLink } from "lucide-react"; // types @@ -30,12 +30,12 @@ import { import { useIssues } from "@/hooks/store/use-issues"; import { usePlatformOS } from "@/hooks/use-platform-os"; -export const ProjectIssuesHeader: React.FC = observer(() => { +const ProjectIssuesHeader: React.FC = observer(() => { // states const [analyticsModal, setAnalyticsModal] = useState(false); // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string }; // store hooks const { project: { projectMemberIds }, @@ -234,3 +234,5 @@ export const ProjectIssuesHeader: React.FC = observer(() => { ); }); + +export default ProjectIssuesHeader; diff --git a/web/components/issues/issues-mobile-header.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/issues/mobile-header.tsx similarity index 97% rename from web/components/issues/issues-mobile-header.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/issues/mobile-header.tsx index 956aa0d29..cab53f88f 100644 --- a/web/components/issues/issues-mobile-header.tsx +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/issues/mobile-header.tsx @@ -1,6 +1,6 @@ import { useCallback, useState } from "react"; import { observer } from "mobx-react"; -import router from "next/router"; +import { useParams } from "next/navigation"; // icons import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; // types @@ -17,14 +17,14 @@ import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store"; -export const IssuesMobileHeader = observer(() => { +const ProjectIssuesMobileHeader = observer(() => { const layouts = [ { key: "list", title: "List", icon: List }, { key: "kanban", title: "Kanban", icon: Kanban }, { key: "calendar", title: "Calendar", icon: Calendar }, ]; const [analyticsModal, setAnalyticsModal] = useState(false); - const { workspaceSlug, projectId } = router.query as { + const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string; }; @@ -180,3 +180,5 @@ export const IssuesMobileHeader = observer(() => { ); }); + +export default ProjectIssuesMobileHeader; diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/issues/page.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/issues/page.tsx new file mode 100644 index 000000000..7c4296d92 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/issues/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import ProjectIssuesHeader from "./header"; +import ProjectIssuesMobileHeader from "./mobile-header"; + +const ProjectIssuesHeaderPage = () => ( + } mobileHeader={} /> +); + +export default ProjectIssuesHeaderPage; diff --git a/web/components/headers/module-issues.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/modules/[moduleId]/header.tsx similarity index 96% rename from web/components/headers/module-issues.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/modules/[moduleId]/header.tsx index a433bead5..474bcd713 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/modules/[moduleId]/header.tsx @@ -1,7 +1,7 @@ import { useCallback, useState } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -import { useRouter } from "next/router"; +import { useParams, useRouter } from "next/navigation"; // icons import { ArrowRight, PanelRight } from "lucide-react"; // types @@ -37,8 +37,7 @@ import { usePlatformOS } from "@/hooks/use-platform-os"; const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => { // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId } = useParams(); // store hooks const { getModuleById } = useModule(); // derived values @@ -59,12 +58,12 @@ const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => { ); }; -export const ModuleIssuesHeader: React.FC = observer(() => { +const ModuleIssuesHeader: React.FC = observer(() => { // states const [analyticsModal, setAnalyticsModal] = useState(false); // router const router = useRouter(); - const { workspaceSlug, projectId, moduleId } = router.query; + const { workspaceSlug, projectId, moduleId } = useParams(); // hooks const { isMobile } = usePlatformOS(); // store hooks @@ -206,9 +205,8 @@ export const ModuleIssuesHeader: React.FC = observer(() => { {issuesCount && issuesCount > 0 ? ( 1 ? "issues" : "issue" - } in this module`} + tooltipContent={`There are ${issuesCount} ${issuesCount > 1 ? "issues" : "issue" + } in this module`} position="bottom" > @@ -310,3 +308,5 @@ export const ModuleIssuesHeader: React.FC = observer(() => { ); }); + +export default ModuleIssuesHeader; diff --git a/web/components/modules/module-mobile-header.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/modules/[moduleId]/mobile-header.tsx similarity index 97% rename from web/components/modules/module-mobile-header.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/modules/[moduleId]/mobile-header.tsx index ababa1ba1..db2e697e5 100644 --- a/web/components/modules/module-mobile-header.tsx +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/modules/[moduleId]/mobile-header.tsx @@ -1,6 +1,6 @@ import { useCallback, useState } from "react"; import { observer } from "mobx-react"; -import router from "next/router"; +import { useParams } from "next/navigation"; // icons import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; // types @@ -17,7 +17,7 @@ import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useIssues, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store"; -export const ModuleMobileHeader = observer(() => { +const ModuleIssuesMobileHeader = observer(() => { const [analyticsModal, setAnalyticsModal] = useState(false); const { currentProjectDetails } = useProject(); const { getModuleById } = useModule(); @@ -26,7 +26,7 @@ export const ModuleMobileHeader = observer(() => { { key: "kanban", title: "Kanban", icon: Kanban }, { key: "calendar", title: "Calendar", icon: Calendar }, ]; - const { workspaceSlug, projectId, moduleId } = router.query as { + const { workspaceSlug, projectId, moduleId } = useParams() as { workspaceSlug: string; projectId: string; moduleId: string; @@ -183,3 +183,5 @@ export const ModuleMobileHeader = observer(() => { ); }); + +export default ModuleIssuesMobileHeader; diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/modules/[moduleId]/page.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/modules/[moduleId]/page.tsx new file mode 100644 index 000000000..43ae996df --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/modules/[moduleId]/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import ModuleIssuesHeader from "./header"; +import ModuleIssuesMobileHeader from "./mobile-header"; + +const ModuleIssuesHeaderPage = () => ( + } mobileHeader={} /> +); + +export default ModuleIssuesHeaderPage; diff --git a/web/components/headers/modules-list.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/modules/header.tsx similarity index 93% rename from web/components/headers/modules-list.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/modules/header.tsx index 0e1fd53fc..884bf1fb2 100644 --- a/web/components/headers/modules-list.tsx +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/modules/header.tsx @@ -1,5 +1,5 @@ import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; +import { useParams, useRouter } from "next/navigation"; // ui import { Breadcrumbs, Button, DiceIcon } from "@plane/ui"; // components @@ -10,10 +10,10 @@ import { EUserProjectRoles } from "@/constants/project"; // hooks import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store"; -export const ModulesListHeader: React.FC = observer(() => { +const ModulesListHeader: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug } = useParams(); // store hooks const { toggleCreateModuleModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); @@ -72,3 +72,5 @@ export const ModulesListHeader: React.FC = observer(() => { ); }); + +export default ModulesListHeader; diff --git a/web/components/modules/moduels-list-mobile-header.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/modules/mobile-header.tsx similarity index 100% rename from web/components/modules/moduels-list-mobile-header.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/modules/mobile-header.tsx diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/modules/page.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/modules/page.tsx new file mode 100644 index 000000000..dce9e103f --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/modules/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import ModulesListHeader from "./header"; +import ModulesListMobileHeader from "./mobile-header"; + +const ModulesHeaderPage = () => ( + } mobileHeader={} /> +); + +export default ModulesHeaderPage; diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/pages/[pageId]/header.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/pages/[pageId]/header.tsx new file mode 100644 index 000000000..f242db9a0 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/pages/[pageId]/header.tsx @@ -0,0 +1,170 @@ +import { useState } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { FileText } from "lucide-react"; +// types +import { TLogoProps } from "@plane/types"; +// ui +import { Breadcrumbs, Button, EmojiIconPicker, EmojiIconPickerTypes, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { BreadcrumbLink, Logo } from "@/components/common"; +// helper +import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper"; +// hooks +import { usePage, useProject } from "@/hooks/store"; +import { usePlatformOS } from "@/hooks/use-platform-os"; + +export interface IPagesHeaderProps { + showButton?: boolean; +} + +const PageDetailsHeader = observer(() => { + // router + const { workspaceSlug, pageId } = useParams(); + // state + const [isOpen, setIsOpen] = useState(false); + // store hooks + const { currentProjectDetails } = useProject(); + const { isContentEditable, isSubmitting, name, logo_props, updatePageLogo } = usePage(pageId?.toString() ?? ""); + + const handlePageLogoUpdate = async (data: TLogoProps) => { + if (data) { + updatePageLogo(data) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Logo Updated successfully.", + }); + }) + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Something went wrong. Please try again.", + }); + }); + } + }; + // use platform + const { platform } = usePlatformOS(); + // derived values + const isMac = platform === "MacOS"; + + return ( +
+
+
+ + + + + + + ) + } + /> + + + + + + } + /> + + } + /> + } + /> + setIsOpen(val)} + className="flex items-center justify-center" + buttonClassName="flex items-center justify-center" + label={ + <> + {logo_props?.in_use ? ( + + ) : ( + + )} + + } + onChange={(val) => { + let logoValue = {}; + + if (val?.type === "emoji") + logoValue = { + value: convertHexEmojiToDecimal(val.value.unified), + url: val.value.imageUrl, + }; + else if (val?.type === "icon") logoValue = val.value; + + handlePageLogoUpdate({ + in_use: val?.type, + [val?.type]: logoValue, + }).finally(() => setIsOpen(false)); + }} + defaultIconColor={ + logo_props?.in_use && logo_props.in_use === "icon" ? logo_props?.icon?.color : undefined + } + defaultOpen={ + logo_props?.in_use && logo_props?.in_use === "emoji" + ? EmojiIconPickerTypes.EMOJI + : EmojiIconPickerTypes.ICON + } + /> + } + /> + } + /> + +
+
+ {isContentEditable && ( + + )} +
+ ); +}); + +export default PageDetailsHeader; diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/pages/[pageId]/page.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/pages/[pageId]/page.tsx new file mode 100644 index 000000000..6a426ea2a --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/pages/[pageId]/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import PageDetailsHeader from "./header"; + +const PageDetailsHeaderPage = () => } />; + +export default PageDetailsHeaderPage; diff --git a/web/components/headers/pages.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/pages/header.tsx similarity index 90% rename from web/components/headers/pages.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/pages/header.tsx index 893c4409d..bd35c875a 100644 --- a/web/components/headers/pages.tsx +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/pages/header.tsx @@ -1,5 +1,5 @@ import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams, useSearchParams } from "next/navigation"; import { FileText } from "lucide-react"; // ui import { Breadcrumbs, Button } from "@plane/ui"; @@ -11,10 +11,11 @@ import { EUserProjectRoles } from "@/constants/project"; // hooks import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store"; -export const PagesHeader = observer(() => { +const PagesHeader = observer(() => { // router - const router = useRouter(); - const { workspaceSlug, type: pageType } = router.query; + const { workspaceSlug } = useParams(); + const searchParams = useSearchParams(); + const pageType = searchParams.get("type"); // store hooks const { toggleCreatePageModal } = useCommandPalette(); const { @@ -74,3 +75,5 @@ export const PagesHeader = observer(() => { ); }); + +export default PagesHeader; diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/pages/page.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/pages/page.tsx new file mode 100644 index 000000000..e59e271c7 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/pages/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import PagesHeader from "./header"; + +const PagesHeaderPage = () => } />; + +export default PagesHeaderPage; diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/settings/[...default-header]/page.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/settings/[...default-header]/page.tsx new file mode 100644 index 000000000..c9c53de65 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/settings/[...default-header]/page.tsx @@ -0,0 +1,2 @@ +import DefaultProjectSettingHeader from "../page"; +export default DefaultProjectSettingHeader; \ No newline at end of file diff --git a/web/components/headers/project-settings.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/settings/header.tsx similarity index 93% rename from web/components/headers/project-settings.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/settings/header.tsx index 2fe48969d..351c20d81 100644 --- a/web/components/headers/project-settings.tsx +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/settings/header.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams, useRouter } from "next/navigation"; // ui import { Settings } from "lucide-react"; import { Breadcrumbs, CustomMenu } from "@plane/ui"; @@ -11,10 +11,10 @@ import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "@/constants/project"; // hooks import { useProject, useUser } from "@/hooks/store"; -export const ProjectSettingHeader: FC = observer(() => { +const ProjectSettingHeader: FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId } = useParams(); // store hooks const { membership: { currentProjectRole }, @@ -80,3 +80,5 @@ export const ProjectSettingHeader: FC = observer(() => { ); }); + +export default ProjectSettingHeader; diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/settings/page.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/settings/page.tsx new file mode 100644 index 000000000..3da6a809a --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/settings/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import ProjectSettingHeader from "./header"; + +const ProjectSettingHeaderPage = () => } />; + +export default ProjectSettingHeaderPage; diff --git a/web/components/headers/project-view-issues.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/views/[viewId]/header.tsx similarity index 97% rename from web/components/headers/project-view-issues.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/views/[viewId]/header.tsx index 6270093a3..da44f2b68 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/views/[viewId]/header.tsx @@ -1,7 +1,7 @@ import { useCallback } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // ui @@ -28,10 +28,9 @@ import { useUser, } from "@/hooks/store"; -export const ProjectViewIssuesHeader: React.FC = observer(() => { +const ProjectViewIssuesHeader: React.FC = observer(() => { // router - const router = useRouter(); - const { workspaceSlug, projectId, viewId } = router.query; + const { workspaceSlug, projectId, viewId } = useParams(); // store hooks const { issuesFilter: { issueFilters, updateFilters }, @@ -256,3 +255,5 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { ); }); + +export default ProjectViewIssuesHeader; diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/views/[viewId]/page.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/views/[viewId]/page.tsx new file mode 100644 index 000000000..14f135d4e --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/views/[viewId]/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import ProjectViewIssuesHeader from "./header"; + +const ProjectViewIssuesHeaderPage = () => } />; + +export default ProjectViewIssuesHeaderPage; diff --git a/web/components/headers/project-views.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/views/header.tsx similarity index 93% rename from web/components/headers/project-views.tsx rename to web/app/[workspaceSlug]/@header/projects/[projectId]/views/header.tsx index 7f1d1a725..edbf406ef 100644 --- a/web/components/headers/project-views.tsx +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/views/header.tsx @@ -1,5 +1,5 @@ import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; // ui import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui"; // components @@ -10,10 +10,9 @@ import { EUserProjectRoles } from "@/constants/project"; // hooks import { useCommandPalette, useProject, useUser } from "@/hooks/store"; -export const ProjectViewsHeader: React.FC = observer(() => { +const ProjectViewsHeader: React.FC = observer(() => { // router - const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug } = useParams(); // store hooks const { toggleCreateViewModal } = useCommandPalette(); const { @@ -69,3 +68,5 @@ export const ProjectViewsHeader: React.FC = observer(() => { ); }); + +export default ProjectViewsHeader; diff --git a/web/app/[workspaceSlug]/@header/projects/[projectId]/views/page.tsx b/web/app/[workspaceSlug]/@header/projects/[projectId]/views/page.tsx new file mode 100644 index 000000000..e5f8ac92a --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/[projectId]/views/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import ProjectViewsHeader from "./header"; + +const ProjectViewsHeaderPage = () => } />; + +export default ProjectViewsHeaderPage; diff --git a/web/components/headers/projects.tsx b/web/app/[workspaceSlug]/@header/projects/header.tsx similarity index 98% rename from web/components/headers/projects.tsx rename to web/app/[workspaceSlug]/@header/projects/header.tsx index 7126b2697..57b0a50cd 100644 --- a/web/components/headers/projects.tsx +++ b/web/app/[workspaceSlug]/@header/projects/header.tsx @@ -18,7 +18,7 @@ import { calculateTotalFilters } from "@/helpers/filter.helper"; import { useAppRouter, useCommandPalette, useEventTracker, useMember, useProjectFilter, useUser } from "@/hooks/store"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; -export const ProjectsHeader = observer(() => { +const ProjectsHeader = observer(() => { // states const [isSearchOpen, setIsSearchOpen] = useState(false); // refs @@ -181,3 +181,5 @@ export const ProjectsHeader = observer(() => { ); }); + +export default ProjectsHeader; diff --git a/web/components/project/projects-mobile-header.tsx b/web/app/[workspaceSlug]/@header/projects/mobile-header.tsx similarity index 100% rename from web/components/project/projects-mobile-header.tsx rename to web/app/[workspaceSlug]/@header/projects/mobile-header.tsx diff --git a/web/app/[workspaceSlug]/@header/projects/page.tsx b/web/app/[workspaceSlug]/@header/projects/page.tsx new file mode 100644 index 000000000..64108a13c --- /dev/null +++ b/web/app/[workspaceSlug]/@header/projects/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +// components +import AppHeaderWrapper from "@/app/[workspaceSlug]/app-header-wrapper"; +import ProjectsHeader from "./header"; +import ProjectsMobileHeader from "./mobile-header"; + + +const ProjectsHeaderPage = () => ( + } mobileHeader={} /> +); + +export default ProjectsHeaderPage; diff --git a/web/app/[workspaceSlug]/@header/settings/[...default-header]/page.tsx b/web/app/[workspaceSlug]/@header/settings/[...default-header]/page.tsx new file mode 100644 index 000000000..9bb709262 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/settings/[...default-header]/page.tsx @@ -0,0 +1,2 @@ +import DefaultWorkspaceSettingsHeader from "../page"; +export default DefaultWorkspaceSettingsHeader; \ No newline at end of file diff --git a/web/components/headers/workspace-settings.tsx b/web/app/[workspaceSlug]/@header/settings/header.tsx similarity index 91% rename from web/components/headers/workspace-settings.tsx rename to web/app/[workspaceSlug]/@header/settings/header.tsx index 2d3e9649e..111cbaef4 100644 --- a/web/components/headers/workspace-settings.tsx +++ b/web/app/[workspaceSlug]/@header/settings/header.tsx @@ -1,3 +1,5 @@ +"use client"; + import { FC } from "react"; import { observer } from "mobx-react";; import { Settings } from "lucide-react"; @@ -8,7 +10,7 @@ import { BreadcrumbLink } from "@/components/common"; // hooks import { useWorkspace } from "@/hooks/store"; -export const WorkspaceSettingHeader: FC = observer(() => { +const WorkspaceSettingHeader: FC = observer(() => { const { currentWorkspace } = useWorkspace(); return ( @@ -33,3 +35,5 @@ export const WorkspaceSettingHeader: FC = observer(() => { ); }); + +export default WorkspaceSettingHeader; diff --git a/web/app/[workspaceSlug]/@header/settings/page.tsx b/web/app/[workspaceSlug]/@header/settings/page.tsx new file mode 100644 index 000000000..d7f4e2b53 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/settings/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +// components +import AppHeaderWrapper from "../../app-header-wrapper"; +import WorkspaceSettingHeader from "./header"; + +const WorkspaceSettingHeaderPage = () => } />; + +export default WorkspaceSettingHeaderPage; diff --git a/web/app/[workspaceSlug]/@header/workspace-views/[...default-header]/page.tsx b/web/app/[workspaceSlug]/@header/workspace-views/[...default-header]/page.tsx new file mode 100644 index 000000000..a057def83 --- /dev/null +++ b/web/app/[workspaceSlug]/@header/workspace-views/[...default-header]/page.tsx @@ -0,0 +1,2 @@ +import DefaultWorkspaceViewsHeader from "../page"; +export default DefaultWorkspaceViewsHeader; \ No newline at end of file diff --git a/web/components/headers/global-issues.tsx b/web/app/[workspaceSlug]/@header/workspace-views/header.tsx similarity index 96% rename from web/components/headers/global-issues.tsx rename to web/app/[workspaceSlug]/@header/workspace-views/header.tsx index a6a35d16f..091847d0a 100644 --- a/web/components/headers/global-issues.tsx +++ b/web/app/[workspaceSlug]/@header/workspace-views/header.tsx @@ -1,6 +1,8 @@ +"use client"; + import { useCallback, useState } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; // ui @@ -17,12 +19,11 @@ import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useLabel, useMember, useUser, useIssues } from "@/hooks/store"; -export const GlobalIssuesHeader: React.FC = observer(() => { +const GlobalIssuesHeader: React.FC = observer(() => { // states const [createViewModal, setCreateViewModal] = useState(false); // router - const router = useRouter(); - const { workspaceSlug, globalViewId } = router.query; + const { workspaceSlug, globalViewId } = useParams(); // store hooks const { issuesFilter: { filters, updateFilters }, @@ -143,3 +144,5 @@ export const GlobalIssuesHeader: React.FC = observer(() => { ); }); + +export default GlobalIssuesHeader; diff --git a/web/app/[workspaceSlug]/@header/workspace-views/page.tsx b/web/app/[workspaceSlug]/@header/workspace-views/page.tsx new file mode 100644 index 000000000..51b81df2b --- /dev/null +++ b/web/app/[workspaceSlug]/@header/workspace-views/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +// components +import AppHeaderWrapper from "../../app-header-wrapper"; +import GlobalIssuesHeader from "./header"; + +const GlobalIssuesHeaderPage = () => } />; + +export default GlobalIssuesHeaderPage; diff --git a/web/pages/[workspaceSlug]/active-cycles.tsx b/web/app/[workspaceSlug]/active-cycles/page.tsx similarity index 50% rename from web/pages/[workspaceSlug]/active-cycles.tsx rename to web/app/[workspaceSlug]/active-cycles/page.tsx index 86bb37fa0..3ead5226b 100644 --- a/web/pages/[workspaceSlug]/active-cycles.tsx +++ b/web/app/[workspaceSlug]/active-cycles/page.tsx @@ -1,17 +1,13 @@ -import { ReactElement } from "react"; +"use client"; + import { observer } from "mobx-react"; // components import { PageHead } from "@/components/core"; -import { WorkspaceActiveCycleHeader } from "@/components/headers"; import { WorkspaceActiveCyclesUpgrade } from "@/components/workspace"; -// layouts -import { useWorkspace } from "@/hooks/store"; -import { AppLayout } from "@/layouts/app-layout"; -// types -import { NextPageWithLayout } from "@/lib/types"; // hooks +import { useWorkspace } from "@/hooks/store"; -const WorkspaceActiveCyclesPage: NextPageWithLayout = observer(() => { +const WorkspaceActiveCyclesPage = observer(() => { const { currentWorkspace } = useWorkspace(); // derived values const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Active Cycles` : undefined; @@ -24,8 +20,4 @@ const WorkspaceActiveCyclesPage: NextPageWithLayout = observer(() => { ); }); -WorkspaceActiveCyclesPage.getLayout = function getLayout(page: ReactElement) { - return }>{page}; -}; - -export default WorkspaceActiveCyclesPage; +export default WorkspaceActiveCyclesPage; \ No newline at end of file diff --git a/web/pages/[workspaceSlug]/analytics.tsx b/web/app/[workspaceSlug]/analytics/page.tsx similarity index 76% rename from web/pages/[workspaceSlug]/analytics.tsx rename to web/app/[workspaceSlug]/analytics/page.tsx index 499efc917..2a4f9b9e9 100644 --- a/web/pages/[workspaceSlug]/analytics.tsx +++ b/web/app/[workspaceSlug]/analytics/page.tsx @@ -1,25 +1,22 @@ -import React, { Fragment, ReactElement } from "react"; +"use client"; + +import React, { Fragment } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useSearchParams } from "next/navigation"; import { Tab } from "@headlessui/react"; // components import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics"; import { PageHead } from "@/components/core"; import { EmptyState } from "@/components/empty-state"; -import { WorkspaceAnalyticsHeader } from "@/components/headers"; // constants import { ANALYTICS_TABS } from "@/constants/analytics"; import { EmptyStateType } from "@/constants/empty-state"; // hooks -import { useCommandPalette, useEventTracker, useProject, useWorkspace } from "@/hooks/store"; -// layouts -import { AppLayout } from "@/layouts/app-layout"; -// types -import { NextPageWithLayout } from "@/lib/types"; +import { useCommandPalette, useEventTracker, useProject, useWorkspace } from "@/hooks/store";; -const AnalyticsPage: NextPageWithLayout = observer(() => { - const router = useRouter(); - const { analytics_tab } = router.query; +const AnalyticsPage = observer(() => { + const searchParams = useSearchParams(); + const analytics_tab = searchParams.get("analytics_tab"); // store hooks const { toggleCreateProjectModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); @@ -39,9 +36,8 @@ const AnalyticsPage: NextPageWithLayout = observer(() => { {({ selected }) => ( + + Back to sign in + + + + + + + + ); +}; + +export default ForgotPasswordPage; diff --git a/web/app/accounts/reset-password/layout.tsx b/web/app/accounts/reset-password/layout.tsx new file mode 100644 index 000000000..dbc0a29b4 --- /dev/null +++ b/web/app/accounts/reset-password/layout.tsx @@ -0,0 +1,9 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Reset Password - Plane", +}; + +export default function ResetPasswordLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/web/app/accounts/reset-password/page.tsx b/web/app/accounts/reset-password/page.tsx new file mode 100644 index 000000000..cd661feb0 --- /dev/null +++ b/web/app/accounts/reset-password/page.tsx @@ -0,0 +1,235 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; +// icons +import { useTheme } from "next-themes"; +import { Eye, EyeOff } from "lucide-react"; +// ui +import { Button, Input } from "@plane/ui"; +// components +import { AuthBanner, PasswordStrengthMeter } from "@/components/account"; +// helpers +import { + EAuthenticationErrorCodes, + EErrorAlertType, + EPageTypes, + TAuthErrorInfo, + authErrorHandler, +} from "@/helpers/authentication.helper"; +import { API_BASE_URL } from "@/helpers/common.helper"; +import { getPasswordStrength } from "@/helpers/password.helper"; +// wrappers +import { AuthenticationWrapper } from "@/lib/wrappers"; +// services +import { AuthService } from "@/services/auth.service"; +// images +import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg"; +import PlaneBackgroundPattern from "public/auth/background-pattern.svg"; +import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.png"; +import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.png"; + +type TResetPasswordFormValues = { + email: string; + password: string; + confirm_password?: string; +}; + +const defaultValues: TResetPasswordFormValues = { + email: "", + password: "", +}; + +// services +const authService = new AuthService(); + +const ResetPasswordPage = () => { + // search params + const searchParams = useSearchParams(); + const uidb64 = searchParams.get("uidb64"); + const token = searchParams.get("token"); + const email = searchParams.get("email"); + const error_code = searchParams.get("error_code"); + // states + const [showPassword, setShowPassword] = useState({ + password: false, + retypePassword: false, + }); + const [resetFormData, setResetFormData] = useState({ + ...defaultValues, + email: email ? email.toString() : "", + }); + const [csrfToken, setCsrfToken] = useState(undefined); + const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); + const [isRetryPasswordInputFocused, setIsRetryPasswordInputFocused] = useState(false); + const [errorInfo, setErrorInfo] = useState(undefined); + + // hooks + const { resolvedTheme } = useTheme(); + + const handleShowPassword = (key: keyof typeof showPassword) => + setShowPassword((prev) => ({ ...prev, [key]: !prev[key] })); + + 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] + ); + + useEffect(() => { + if (error_code) { + const errorhandler = authErrorHandler(error_code?.toString() as EAuthenticationErrorCodes); + if (errorhandler) { + setErrorInfo(errorhandler); + } + } + }, [error_code]); + + const password = resetFormData?.password ?? ""; + const confirmPassword = resetFormData?.confirm_password ?? ""; + const renderPasswordMatchError = !isRetryPasswordInputFocused || confirmPassword.length >= password.length; + + const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo; + + return ( + +
+
+ Plane background pattern +
+
+
+
+ + Plane logo + +
+
+
+
+
+

+ Set new password +

+

Secure your account with a strong password

+
+ {errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && ( + setErrorInfo(value)} /> + )} +
+ +
+ +
+ +
+
+
+ +
+ 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.password ? ( + handleShowPassword("password")} + /> + ) : ( + handleShowPassword("password")} + /> + )} +
+ {isPasswordInputFocused && } +
+
+ +
+ 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" + onFocus={() => setIsRetryPasswordInputFocused(true)} + onBlur={() => setIsRetryPasswordInputFocused(false)} + /> + {showPassword.retypePassword ? ( + handleShowPassword("retypePassword")} + /> + ) : ( + handleShowPassword("retypePassword")} + /> + )} +
+ {!!resetFormData.confirm_password && + resetFormData.password !== resetFormData.confirm_password && + renderPasswordMatchError && Passwords don{"'"}t match} +
+ +
+
+
+
+
+
+ ); +}; + +export default ResetPasswordPage; diff --git a/web/app/accounts/set-password/layout.tsx b/web/app/accounts/set-password/layout.tsx new file mode 100644 index 000000000..dbd32e9e8 --- /dev/null +++ b/web/app/accounts/set-password/layout.tsx @@ -0,0 +1,9 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Set Password - Plane", +}; + +export default function SetPasswordLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/web/app/accounts/set-password/page.tsx b/web/app/accounts/set-password/page.tsx new file mode 100644 index 000000000..26553e87b --- /dev/null +++ b/web/app/accounts/set-password/page.tsx @@ -0,0 +1,228 @@ +"use client"; + +import { FormEvent, useEffect, useMemo, useState } from "react"; +import { observer } from "mobx-react-lite"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +// icons +import { useTheme } from "next-themes"; +import { Eye, EyeOff } from "lucide-react"; +// ui +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { PasswordStrengthMeter } from "@/components/account"; +// helpers +import { EPageTypes } from "@/helpers/authentication.helper"; +import { getPasswordStrength } from "@/helpers/password.helper"; +// hooks +import { useUser } from "@/hooks/store"; +// wrappers +import { AuthenticationWrapper } from "@/lib/wrappers"; +// services +import { AuthService } from "@/services/auth.service"; +// images +import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg"; +import PlaneBackgroundPattern from "public/auth/background-pattern.svg"; +import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.png"; +import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.png"; + +type TResetPasswordFormValues = { + email: string; + password: string; + confirm_password?: string; +}; + +const defaultValues: TResetPasswordFormValues = { + email: "", + password: "", +}; + +// services +const authService = new AuthService(); + +const SetPasswordPage = observer(() => { + // router + const router = useRouter(); + // search params + const searchParams = useSearchParams(); + const email = searchParams.get("email"); + // states + const [showPassword, setShowPassword] = useState({ + password: false, + retypePassword: false, + }); + const [passwordFormData, setPasswordFormData] = useState({ + ...defaultValues, + email: email ? email.toString() : "", + }); + const [csrfToken, setCsrfToken] = useState(undefined); + const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); + const [isRetryPasswordInputFocused, setIsRetryPasswordInputFocused] = useState(false); + // hooks + const { resolvedTheme } = useTheme(); + // hooks + const { data: user, handleSetPassword } = useUser(); + + useEffect(() => { + if (csrfToken === undefined) + authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token)); + }, [csrfToken]); + + const handleShowPassword = (key: keyof typeof showPassword) => + setShowPassword((prev) => ({ ...prev, [key]: !prev[key] })); + + const handleFormChange = (key: keyof TResetPasswordFormValues, value: string) => + setPasswordFormData((prev) => ({ ...prev, [key]: value })); + + const isButtonDisabled = useMemo( + () => + !!passwordFormData.password && + getPasswordStrength(passwordFormData.password) >= 3 && + passwordFormData.password === passwordFormData.confirm_password + ? false + : true, + [passwordFormData] + ); + + const handleSubmit = async (e: FormEvent) => { + try { + e.preventDefault(); + if (!csrfToken) throw new Error("csrf token not found"); + await handleSetPassword(csrfToken, { password: passwordFormData.password }); + router.push("/"); + } catch (err: any) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: err?.error ?? "Something went wrong. Please try again.", + }); + } + }; + + const password = passwordFormData?.password ?? ""; + const confirmPassword = passwordFormData?.confirm_password ?? ""; + const renderPasswordMatchError = !isRetryPasswordInputFocused || confirmPassword.length >= password.length; + + const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo; + + return ( + +
+
+ Plane background pattern +
+
+
+
+ + Plane logo + +
+
+
+
+
+

+ Secure your account +

+

Setting password helps you login securely

+
+
handleSubmit(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.password ? ( + handleShowPassword("password")} + /> + ) : ( + handleShowPassword("password")} + /> + )} +
+ {isPasswordInputFocused && } +
+
+ +
+ 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" + onFocus={() => setIsRetryPasswordInputFocused(true)} + onBlur={() => setIsRetryPasswordInputFocused(false)} + /> + {showPassword.retypePassword ? ( + handleShowPassword("retypePassword")} + /> + ) : ( + handleShowPassword("retypePassword")} + /> + )} +
+ {!!passwordFormData.confirm_password && + passwordFormData.password !== passwordFormData.confirm_password && + renderPasswordMatchError && Passwords don{"'"}t match} +
+ +
+
+
+
+
+
+ ); +}); + +export default SetPasswordPage; diff --git a/web/app/create-workspace/layout.tsx b/web/app/create-workspace/layout.tsx new file mode 100644 index 000000000..32a220df7 --- /dev/null +++ b/web/app/create-workspace/layout.tsx @@ -0,0 +1,9 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Create Workspace", +}; + +export default function CreateWorkspaceLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/web/pages/create-workspace.tsx b/web/app/create-workspace/page.tsx similarity index 84% rename from web/pages/create-workspace.tsx rename to web/app/create-workspace/page.tsx index e5992e008..f2f92a771 100644 --- a/web/pages/create-workspace.tsx +++ b/web/app/create-workspace/page.tsx @@ -1,26 +1,23 @@ -import { ReactElement, useState } from "react"; +"use client"; + +import { useState } from "react"; import { observer } from "mobx-react"; import Image from "next/image"; import Link from "next/link"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import { useTheme } from "next-themes"; import { IWorkspace } from "@plane/types"; -// hooks -import { PageHead } from "@/components/core"; -import { CreateWorkspaceForm } from "@/components/workspace"; -import { useUser, useUserProfile } from "@/hooks/store"; -// layouts -import DefaultLayout from "@/layouts/default-layout"; // components -// images -import { NextPageWithLayout } from "@/lib/types"; +import { CreateWorkspaceForm } from "@/components/workspace"; +// hooks +import { useUser, useUserProfile } from "@/hooks/store"; // wrappers import { AuthenticationWrapper } from "@/lib/wrappers"; +// images import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.png"; import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.png"; -// types -const CreateWorkspacePage: NextPageWithLayout = observer(() => { +const CreateWorkspacePage = observer(() => { // router const router = useRouter(); // store hooks @@ -42,8 +39,7 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => { const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo; return ( - <> - +
@@ -72,16 +68,8 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => {
- +
); }); -CreateWorkspacePage.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - export default CreateWorkspacePage; diff --git a/web/app/error.tsx b/web/app/error.tsx new file mode 100644 index 000000000..515a107fd --- /dev/null +++ b/web/app/error.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { useEffect } from "react"; +import * as Sentry from "@sentry/nextjs"; +// import { useRouter } from "next/navigation"; +// services +import { Button } from "@plane/ui"; +// helpers +// import { API_BASE_URL } from "@/helpers/common.helper"; +// layouts +import DefaultLayout from "@/layouts/default-layout"; +// +// import { AuthService } from "@/services/auth.service"; +// layouts +// ui + +// services +// const authService = new AuthService(); + +type props = { + error: Error & { digest?: string }; +}; + +const CustomErrorComponent = ({ error }: props) => { + // const router = useRouter(); + + useEffect(() => { + Sentry.captureException(error); + }, [error]); + + const handleRefresh = () => { + window.location.reload(); + }; + + const handleSignOut = async () => { + // await authService + // .signOut(API_BASE_URL) + // .catch(() => + // setToast({ + // type: TOAST_TYPE.ERROR, + // title: "Error!", + // message: "Failed to sign out. Please try again.", + // }) + // ) + // .finally(() => router.push("/")); + }; + + return ( + +
+
+
+
+

Exception Detected!

+

+ We{"'"}re Sorry! An exception has been detected, and our engineering team has been notified. We + apologize for any inconvenience this may have caused. Please reach out to our engineering team at{" "} + + support@plane.so + {" "} + or on our{" "} + + Discord + {" "} + server for further assistance. +

+
+
+ + +
+
+
+
+
+ ); +}; + +export default CustomErrorComponent; diff --git a/web/app/installations/[provider]/layout.tsx b/web/app/installations/[provider]/layout.tsx new file mode 100644 index 000000000..51978de9e --- /dev/null +++ b/web/app/installations/[provider]/layout.tsx @@ -0,0 +1,3 @@ +export default function InstallationProviderLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/web/pages/installations/[provider]/index.tsx b/web/app/installations/[provider]/page.tsx similarity index 82% rename from web/pages/installations/[provider]/index.tsx rename to web/app/installations/[provider]/page.tsx index eb2e850c7..78e8f682d 100644 --- a/web/pages/installations/[provider]/index.tsx +++ b/web/app/installations/[provider]/page.tsx @@ -1,18 +1,23 @@ -import React, { useEffect, ReactElement } from "react"; -import { useRouter } from "next/router"; +"use client"; + +import React, { useEffect } from "react"; +import { useParams, useSearchParams } from "next/navigation"; // ui import { LogoSpinner } from "@/components/common"; -// types -import { NextPageWithLayout } from "@/lib/types"; // services import { AppInstallationService } from "@/services/app_installation.service"; // services const appInstallationService = new AppInstallationService(); -const AppPostInstallation: NextPageWithLayout = () => { - const router = useRouter(); - const { installation_id, state, provider, code } = router.query; +const AppPostInstallation = () => { + // params + const { provider } = useParams(); + // query params + const searchParams = useSearchParams(); + const installation_id = searchParams.get("installation_id"); + const state = searchParams.get("state"); + const code = searchParams.get("code"); useEffect(() => { if (provider === "github" && state && installation_id) { @@ -69,8 +74,4 @@ const AppPostInstallation: NextPageWithLayout = () => { ); }; -AppPostInstallation.getLayout = function getLayout(page: ReactElement) { - return
{page}
; -}; - export default AppPostInstallation; diff --git a/web/app/invitations/layout.tsx b/web/app/invitations/layout.tsx new file mode 100644 index 000000000..2d9a7e688 --- /dev/null +++ b/web/app/invitations/layout.tsx @@ -0,0 +1,9 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Invitations", +}; + +export default function InvitationsLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/web/pages/invitations/index.tsx b/web/app/invitations/page.tsx similarity index 93% rename from web/pages/invitations/index.tsx rename to web/app/invitations/page.tsx index aac6c833b..c15b25d1a 100644 --- a/web/pages/invitations/index.tsx +++ b/web/app/invitations/page.tsx @@ -1,8 +1,10 @@ -import React, { useState, ReactElement } from "react"; +"use client"; + +import React, { useState } from "react"; import { observer } from "mobx-react-lite"; import Image from "next/image"; import Link from "next/link"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import { useTheme } from "next-themes"; import useSWR, { mutate } from "swr"; // icons @@ -13,7 +15,6 @@ import type { IWorkspaceMemberInvitation } from "@plane/types"; import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // components import { EmptyState } from "@/components/common"; -import { PageHead } from "@/components/core"; // constants import { MEMBER_ACCEPTED } from "@/constants/event-tracker"; import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys"; @@ -22,12 +23,8 @@ import { ROLE } from "@/constants/workspace"; import { truncateText } from "@/helpers/string.helper"; import { getUserRole } from "@/helpers/user.helper"; import { useEventTracker, useUser, useUserProfile, useWorkspace } from "@/hooks/store"; -import DefaultLayout from "@/layouts/default-layout"; -// types -import { NextPageWithLayout } from "@/lib/types"; -// wrappers -import { AuthenticationWrapper } from "@/lib/wrappers"; // services +import { AuthenticationWrapper } from "@/lib/wrappers"; import { WorkspaceService } from "@/services/workspace.service"; // images import emptyInvitation from "public/empty-state/invitation.svg"; @@ -36,7 +33,7 @@ import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-l const workspaceService = new WorkspaceService(); -const UserInvitationsPage: NextPageWithLayout = observer(() => { +const UserInvitationsPage = observer(() => { // states const [invitationsRespond, setInvitationsRespond] = useState([]); const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false); @@ -130,8 +127,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo; return ( - <> - +
@@ -157,11 +153,10 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { return (
handleInvitation(invitation, isSelected ? "withdraw" : "accepted")} >
@@ -230,16 +225,8 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { ) ) : null}
- + ); }); -UserInvitationsPage.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - export default UserInvitationsPage; diff --git a/web/app/layout.tsx b/web/app/layout.tsx new file mode 100644 index 000000000..49d160412 --- /dev/null +++ b/web/app/layout.tsx @@ -0,0 +1,61 @@ +import { Metadata } from "next"; +import Script from "next/script"; +// styles +import "@/styles/globals.css"; +import "@/styles/command-pallette.css"; +import "@/styles/nprogress.css"; +import "@/styles/emoji.css"; +import "@/styles/react-day-picker.css"; +// local +import { AppProvider } from "./provider"; + +export const metadata: Metadata = { + title: "Plane | Simple, extensible, open-source project management tool.", + description: + "Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.", + openGraph: { + title: "Plane | Simple, extensible, open-source project management tool.", + description: "Plane Deploy is a customer feedback management tool built on top of plane.so", + url: "https://app.plane.so/", + }, + keywords: + "software development, plan, ship, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration", + twitter: { + site: "@planepowers", + }, +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + const isSessionRecorderEnabled = parseInt(process.env.NEXT_PUBLIC_ENABLE_SESSION_RECORDER || "0"); + + return ( + + + + + + + + + + +
+ +
{children}
+
+ + {process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN && ( + + )} + + ); +} diff --git a/web/pages/404.tsx b/web/app/not-found.tsx similarity index 78% rename from web/pages/404.tsx rename to web/app/not-found.tsx index 514169dea..10040faab 100644 --- a/web/pages/404.tsx +++ b/web/app/not-found.tsx @@ -1,21 +1,20 @@ -import React from "react"; +"use client"; -import type { NextPage } from "next"; +import React from "react"; +import { Metadata } from "next"; import Image from "next/image"; import Link from "next/link"; -// components -import { Button } from "@plane/ui"; -import { PageHead } from "@/components/core"; -// layouts -import DefaultLayout from "@/layouts/default-layout"; // ui +import { Button } from "@plane/ui"; // images import Image404 from "public/404.svg"; -// types -const PageNotFound: NextPage = () => ( - - +export const metadata: Metadata = { + title: "404 - Page Not Found", +}; + +const PageNotFound = () => ( +
@@ -37,7 +36,7 @@ const PageNotFound: NextPage = () => (
- +
); export default PageNotFound; diff --git a/web/app/onboarding/layout.tsx b/web/app/onboarding/layout.tsx new file mode 100644 index 000000000..492ebc402 --- /dev/null +++ b/web/app/onboarding/layout.tsx @@ -0,0 +1,9 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Onboarding", +}; + +export default function OnboardingLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/web/pages/onboarding/index.tsx b/web/app/onboarding/page.tsx similarity index 90% rename from web/pages/onboarding/index.tsx rename to web/app/onboarding/page.tsx index 89263e57d..0e95e0f47 100644 --- a/web/pages/onboarding/index.tsx +++ b/web/app/onboarding/page.tsx @@ -1,24 +1,20 @@ -import { ReactElement, useEffect, useState } from "react"; +"use client"; + +import { useEffect, useState } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import useSWR from "swr"; // types +import { EPageTypes } from "@plane/constants"; import { TOnboardingSteps, TUserProfile } from "@plane/types"; // components import { LogoSpinner } from "@/components/common"; -import { PageHead } from "@/components/core"; import { InviteMembers, CreateOrJoinWorkspaces, ProfileSetup } from "@/components/onboarding"; // constants import { USER_ONBOARDING_COMPLETED } from "@/constants/event-tracker"; import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys"; -// helpers -import { EPageTypes } from "@/helpers/authentication.helper"; // hooks import { useUser, useWorkspace, useUserProfile, useEventTracker } from "@/hooks/store"; -// layouts -import DefaultLayout from "@/layouts/default-layout"; -// lib types -import { NextPageWithLayout } from "@/lib/types"; // wrappers import { AuthenticationWrapper } from "@/lib/wrappers"; // services @@ -32,7 +28,7 @@ export enum EOnboardingSteps { const workspaceService = new WorkspaceService(); -const OnboardingPage: NextPageWithLayout = observer(() => { +const OnboardingPage = observer(() => { // states const [step, setStep] = useState(null); const [totalSteps, setTotalSteps] = useState(null); @@ -143,8 +139,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => { }, [user, step, profile.onboarding_step, updateCurrentUser, workspacesList]); return ( - <> - + {user && totalSteps && step !== null && invitations ? (
{step === EOnboardingSteps.PROFILE_SETUP ? ( @@ -179,16 +174,8 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
)} - +
); }); -OnboardingPage.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - export default OnboardingPage; diff --git a/web/app/page.tsx b/web/app/page.tsx new file mode 100644 index 000000000..8c502a7db --- /dev/null +++ b/web/app/page.tsx @@ -0,0 +1,77 @@ +"use client"; + +import React from "react"; +import { observer } from "mobx-react"; +import Image from "next/image"; +import Link from "next/link"; +// ui +import { useTheme } from "next-themes"; +// components +import { AuthRoot } from "@/components/account"; +import { PageHead } from "@/components/core"; +// constants +import { NAVIGATE_TO_SIGNUP } from "@/constants/event-tracker"; +// helpers +import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper"; +// hooks +import { useEventTracker } from "@/hooks/store"; +// layouts +import DefaultLayout from "@/layouts/default-layout"; +// wrappers +import { AuthenticationWrapper } from "@/lib/wrappers"; +// assets +import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg"; +import PlaneBackgroundPattern from "public/auth/background-pattern.svg"; +import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.png"; +import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.png"; + +const HomePage = observer(() => { + const { resolvedTheme } = useTheme(); + // hooks + const { captureEvent } = useEventTracker(); + + const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo; + + return ( + + + <> +
+ +
+ Plane background pattern +
+
+
+
+ + Plane logo + +
+
+ New to Plane?{" "} + captureEvent(NAVIGATE_TO_SIGNUP, {})} + className="font-semibold text-custom-primary-100 hover:underline" + > + Create an account + +
+
+
+ +
+
+
+ +
+
+ ); +}); + +export default HomePage; diff --git a/web/pages/profile/activity.tsx b/web/app/profile/activity/page.tsx similarity index 88% rename from web/pages/profile/activity.tsx rename to web/app/profile/activity/page.tsx index 1f724b4ff..c9f089e09 100644 --- a/web/pages/profile/activity.tsx +++ b/web/app/profile/activity/page.tsx @@ -1,4 +1,6 @@ -import { ReactElement, useState } from "react"; +"use client"; + +import { useState } from "react"; import { observer } from "mobx-react"; // ui import { Button } from "@plane/ui"; @@ -18,7 +20,7 @@ import { NextPageWithLayout } from "@/lib/types"; const PER_PAGE = 100; -const ProfileActivityPage: NextPageWithLayout = observer(() => { +const ProfileActivityPage = observer(() => { // states const [pageCount, setPageCount] = useState(1); const [totalPages, setTotalPages] = useState(0); @@ -59,7 +61,7 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
- toggleSidebar()} /> +

Activity

@@ -77,8 +79,4 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => { ); }); -ProfileActivityPage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - export default ProfileActivityPage; diff --git a/web/pages/profile/preferences/theme.tsx b/web/app/profile/appearance/page.tsx similarity index 77% rename from web/pages/profile/preferences/theme.tsx rename to web/app/profile/appearance/page.tsx index f35fe72b8..e34fe8cdd 100644 --- a/web/pages/profile/preferences/theme.tsx +++ b/web/app/profile/appearance/page.tsx @@ -1,21 +1,19 @@ -import { useEffect, useState, ReactElement } from "react"; +"use client"; + +import { useEffect, useState } from "react"; import { observer } from "mobx-react"; import { useTheme } from "next-themes"; // ui import { setPromiseToast } from "@plane/ui"; // components import { LogoSpinner } from "@/components/common"; -import { CustomThemeSelector, ThemeSwitch, PageHead } from "@/components/core"; +import { CustomThemeSelector, ThemeSwitch, PageHead, SidebarHamburgerToggle } from "@/components/core"; // constants import { I_THEME_OPTION, THEME_OPTIONS } from "@/constants/themes"; // hooks import { useUserProfile } from "@/hooks/store"; -// layouts -import { ProfilePreferenceSettingsLayout } from "@/layouts/settings-layout/profile/preferences"; -// type -import { NextPageWithLayout } from "@/lib/types"; -const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => { +const ProfileAppearancePage = observer(() => { const { setTheme } = useTheme(); // states const [currentTheme, setCurrentTheme] = useState(null); @@ -53,8 +51,9 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => { {userProfile ? (
-
-

Preferences

+
+ +

Appearance

@@ -76,8 +75,4 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => { ); }); -ProfilePreferencesThemePage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default ProfilePreferencesThemePage; +export default ProfileAppearancePage; \ No newline at end of file diff --git a/web/layouts/settings-layout/profile/layout.tsx b/web/app/profile/layout.tsx similarity index 67% rename from web/layouts/settings-layout/profile/layout.tsx rename to web/app/profile/layout.tsx index e2d11155f..1f1b1dff4 100644 --- a/web/layouts/settings-layout/profile/layout.tsx +++ b/web/app/profile/layout.tsx @@ -1,18 +1,19 @@ -import { FC, ReactNode } from "react"; -// layout +"use client"; + +import { ReactNode } from "react"; +// components import { CommandPalette } from "@/components/command-palette"; -import { ProfileLayoutSidebar } from "@/layouts/settings-layout"; // wrappers import { AuthenticationWrapper } from "@/lib/wrappers"; -// components +// layout +import { ProfileLayoutSidebar } from "./sidebar"; -interface IProfileSettingsLayout { +type Props = { children: ReactNode; - header?: ReactNode; -} +}; -export const ProfileSettingsLayout: FC = (props) => { - const { children, header } = props; +export default function ProfileSettingsLayout(props: Props) { + const { children } = props; return ( <> @@ -21,11 +22,10 @@ export const ProfileSettingsLayout: FC = (props) => {
- {header}
{children}
); -}; +} diff --git a/web/pages/profile/preferences/email.tsx b/web/app/profile/notifications/page.tsx similarity index 51% rename from web/pages/profile/preferences/email.tsx rename to web/app/profile/notifications/page.tsx index 69dcb6ee6..8e8ff3f04 100644 --- a/web/pages/profile/preferences/email.tsx +++ b/web/app/profile/notifications/page.tsx @@ -1,21 +1,20 @@ -import { ReactElement } from "react"; +"use client"; + import useSWR from "swr"; // layouts -import { PageHead } from "@/components/core"; -import { EmailNotificationForm } from "@/components/profile/preferences"; +import { PageHead, SidebarHamburgerToggle } from "@/components/core"; +import { EmailNotificationForm } from "@/components/profile/notification"; import { EmailSettingsLoader } from "@/components/ui"; -import { ProfilePreferenceSettingsLayout } from "@/layouts/settings-layout/profile/preferences"; // ui // components // services -import { NextPageWithLayout } from "@/lib/types"; import { UserService } from "@/services/user.service"; // type // services const userService = new UserService(); -const ProfilePreferencesThemePage: NextPageWithLayout = () => { +const ProfileNotificationPage = () => { // fetching user email notification settings const { data, isLoading } = useSWR("CURRENT_USER_EMAIL_NOTIFICATION_SETTINGS", () => userService.currentUserEmailNotificationSettings() @@ -29,14 +28,19 @@ const ProfilePreferencesThemePage: NextPageWithLayout = () => { <>
+
+ +
+
Email notifications
+
+ Stay in the loop on Issues you are subscribed to. Enable this to get notified. +
+
+
); }; -ProfilePreferencesThemePage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default ProfilePreferencesThemePage; +export default ProfileNotificationPage; \ No newline at end of file diff --git a/web/pages/profile/index.tsx b/web/app/profile/page.tsx similarity index 97% rename from web/pages/profile/index.tsx rename to web/app/profile/page.tsx index a7415cf3e..ebc0dfaf7 100644 --- a/web/pages/profile/index.tsx +++ b/web/app/profile/page.tsx @@ -1,4 +1,6 @@ -import React, { useEffect, useState, ReactElement } from "react"; +"use client"; + +import React, { useEffect, useState } from "react"; import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; import { ChevronDown, CircleUserRound } from "lucide-react"; @@ -20,11 +22,9 @@ import { SidebarHamburgerToggle } from "@/components/core/sidebar"; import { TIME_ZONES } from "@/constants/timezones"; import { USER_ROLES } from "@/constants/workspace"; // hooks -import { useAppTheme, useUser } from "@/hooks/store"; -import { ProfileSettingsLayout } from "@/layouts/settings-layout"; +import { useUser } from "@/hooks/store"; +// import { ProfileSettingsLayout } from "@/layouts/settings-layout"; // layouts -// lib types -import type { NextPageWithLayout } from "@/lib/types"; import { FileService } from "@/services/file.service"; // services // types @@ -42,7 +42,7 @@ const defaultValues: Partial = { const fileService = new FileService(); -const ProfileSettingsPage: NextPageWithLayout = observer(() => { +const ProfileSettingsPage = observer(() => { // states const [isLoading, setIsLoading] = useState(false); const [isRemoving, setIsRemoving] = useState(false); @@ -58,7 +58,6 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { } = useForm({ defaultValues }); // store hooks const { data: currentUser, updateCurrentUser } = useUser(); - const { toggleSidebar } = useAppTheme(); useEffect(() => { reset({ ...defaultValues, ...currentUser }); @@ -135,7 +134,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
- toggleSidebar()} /> +
{ ); }); -ProfileSettingsPage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; +// ProfileSettingsPage.getLayout = function getLayout(page: ReactElement) { +// return {page}; +// }; export default ProfileSettingsPage; diff --git a/web/pages/profile/change-password.tsx b/web/app/profile/security/page.tsx similarity index 93% rename from web/pages/profile/change-password.tsx rename to web/app/profile/security/page.tsx index 2ddc02090..121ecb85f 100644 --- a/web/pages/profile/change-password.tsx +++ b/web/app/profile/security/page.tsx @@ -1,6 +1,8 @@ -import { ReactElement, useEffect, useState } from "react"; +"use client"; + +import { useEffect, useState } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff } from "lucide-react"; // ui @@ -14,11 +16,7 @@ import { SidebarHamburgerToggle } from "@/components/core/sidebar"; import { authErrorHandler } from "@/helpers/authentication.helper"; import { getPasswordStrength } from "@/helpers/password.helper"; // hooks -import { useAppTheme, useUser } from "@/hooks/store"; -// layout -import { ProfileSettingsLayout } from "@/layouts/settings-layout"; -// types -import { NextPageWithLayout } from "@/lib/types"; +import { useUser } from "@/hooks/store"; // services import { AuthService } from "@/services/auth.service"; import { UserService } from "@/services/user.service"; @@ -44,7 +42,7 @@ const defaultShowPassword = { confirmPassword: false, }; -const ChangePasswordPage: NextPageWithLayout = observer(() => { +const SecurityPage = observer(() => { // states const [isPageLoading, setIsPageLoading] = useState(true); const [showPassword, setShowPassword] = useState(defaultShowPassword); @@ -52,8 +50,6 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => { const [isRetryPasswordInputFocused, setIsRetryPasswordInputFocused] = useState(false); // router const router = useRouter(); - // store hooks - const { toggleSidebar } = useAppTheme(); const { data: currentUser } = useUser(); // use form const { @@ -136,7 +132,7 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
- toggleSidebar()} /> +
{ ); }); -ChangePasswordPage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default ChangePasswordPage; +export default SecurityPage; \ No newline at end of file diff --git a/web/layouts/settings-layout/profile/sidebar.tsx b/web/app/profile/sidebar.tsx similarity index 98% rename from web/layouts/settings-layout/profile/sidebar.tsx rename to web/app/profile/sidebar.tsx index 9e7fc95cc..f3f87f8e4 100644 --- a/web/layouts/settings-layout/profile/sidebar.tsx +++ b/web/app/profile/sidebar.tsx @@ -1,7 +1,9 @@ +"use client"; + import { useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -import { useRouter } from "next/router"; +import { usePathname } from "next/navigation"; // icons import { ChevronLeft, LogOut, MoveLeft, Plus, UserPlus } from "lucide-react"; // ui @@ -32,7 +34,7 @@ export const ProfileLayoutSidebar = observer(() => { // states const [isSigningOut, setIsSigningOut] = useState(false); // router - const router = useRouter(); + const pathname = usePathname(); // store hooks const { sidebarCollapsed, toggleSidebar } = useAppTheme(); const { data: currentUser, signOut } = useUser(); @@ -133,7 +135,7 @@ export const ProfileLayoutSidebar = observer(() => { >
import("@/lib/wrappers/store-wrapper"), { ssr: false }); +const PostHogProvider = dynamic(() => import("@/lib/posthog-provider"), { ssr: false }); +const CrispWrapper = dynamic(() => import("@/lib/wrappers/crisp-wrapper"), { ssr: false }); +// nprogress +NProgress.configure({ showSpinner: false }); +// Router.events.on("routeChangeStart", NProgress.start); +// Router.events.on("routeChangeError", NProgress.done); +// Router.events.on("routeChangeComplete", NProgress.done); + +export interface IAppProvider { + children: ReactNode; +} + +export const AppProvider: FC = (props) => { + const { children } = props; + // themes + const { resolvedTheme } = useTheme(); + return ( + <> + + + + + + + + {children} + + + + + + + + ); +}; diff --git a/web/app/sign-up/layout.tsx b/web/app/sign-up/layout.tsx new file mode 100644 index 000000000..f7f405c27 --- /dev/null +++ b/web/app/sign-up/layout.tsx @@ -0,0 +1,9 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Sign up - Plane", +}; + +export default function SignUpLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/web/app/sign-up/page.tsx b/web/app/sign-up/page.tsx new file mode 100644 index 000000000..073277d35 --- /dev/null +++ b/web/app/sign-up/page.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { observer } from "mobx-react"; +import Image from "next/image"; +import Link from "next/link"; +// ui +import { useTheme } from "next-themes"; +// components +import { AuthRoot } from "@/components/account"; +// constants +import { NAVIGATE_TO_SIGNIN } from "@/constants/event-tracker"; +// helpers +import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper"; +// hooks +import { useEventTracker } from "@/hooks/store"; +// assets +import { AuthenticationWrapper } from "@/lib/wrappers"; +import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg"; +import PlaneBackgroundPattern from "public/auth/background-pattern.svg"; +import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.png"; +import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.png"; + +export type AuthType = "sign-in" | "sign-up"; + +const SignInPage = observer(() => { + // store hooks + const { captureEvent } = useEventTracker(); + // hooks + const { resolvedTheme } = useTheme(); + + const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo; + + return ( + +
+
+ Plane background pattern +
+
+
+
+ + Plane logo + +
+
+ Already have an account?{" "} + captureEvent(NAVIGATE_TO_SIGNIN, {})} + className="font-semibold text-custom-primary-100 hover:underline" + > + Log in + +
+
+
+ +
+
+
+
+ ); +}); + +export default SignInPage; diff --git a/web/app/workspace-invitations/layout.tsx b/web/app/workspace-invitations/layout.tsx new file mode 100644 index 000000000..8361dddfa --- /dev/null +++ b/web/app/workspace-invitations/layout.tsx @@ -0,0 +1,9 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Workspace Invitations", +}; + +export default function WorkspaceInvitationsLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/web/app/workspace-invitations/page.tsx b/web/app/workspace-invitations/page.tsx new file mode 100644 index 000000000..2c958fdeb --- /dev/null +++ b/web/app/workspace-invitations/page.tsx @@ -0,0 +1,127 @@ +"use client"; + +import React from "react"; +import { observer } from "mobx-react"; +import { useRouter, useSearchParams } from "next/navigation"; +import useSWR from "swr"; +import { Boxes, Check, Share2, Star, User2, X } from "lucide-react"; +// components +import { LogoSpinner } from "@/components/common"; +import { EmptySpace, EmptySpaceItem } from "@/components/ui/empty-space"; +// constants +import { WORKSPACE_INVITATION } from "@/constants/fetch-keys"; +// helpers +import { EPageTypes } from "@/helpers/authentication.helper"; +// hooks +import { useUser } from "@/hooks/store"; +// wrappers +import { AuthenticationWrapper } from "@/lib/wrappers"; +// services +import { WorkspaceService } from "@/services/workspace.service"; + +// service initialization +const workspaceService = new WorkspaceService(); + +const WorkspaceInvitationPage = observer(() => { + // router + const router = useRouter(); + // query params + const searchParams = useSearchParams(); + const invitation_id = searchParams.get("invitation_id"); + const email = searchParams.get("email"); + const slug = searchParams.get("slug"); + // store hooks + const { data: currentUser } = useUser(); + + const { data: invitationDetail, error } = useSWR( + invitation_id && slug && WORKSPACE_INVITATION(invitation_id.toString()), + invitation_id && slug + ? () => workspaceService.getWorkspaceInvitation(slug.toString(), invitation_id.toString()) + : null + ); + + const handleAccept = () => { + if (!invitationDetail) return; + workspaceService + .joinWorkspace(invitationDetail.workspace.slug, invitationDetail.id, { + accepted: true, + email: invitationDetail.email, + }) + .then(() => { + if (email === currentUser?.email) { + router.push("/invitations"); + } else { + router.push(`/?${searchParams.toString()}`); + } + }) + .catch((err) => console.error(err)); + }; + + const handleReject = () => { + if (!invitationDetail) return; + workspaceService + .joinWorkspace(invitationDetail.workspace.slug, invitationDetail.id, { + accepted: false, + email: invitationDetail.email, + }) + .then(() => { + router.push("/"); + }) + .catch((err) => console.error(err)); + }; + + return ( + +
+ {invitationDetail && !invitationDetail.responded_at ? ( + error ? ( +
+

INVITATION NOT FOUND

+
+ ) : ( + + + + + ) + ) : error || invitationDetail?.responded_at ? ( + invitationDetail?.accepted ? ( + + + + ) : ( + + {!currentUser ? ( + + ) : ( + + )} + + + + ) + ) : ( +
+ +
+ )} +
+
+ ); +}); + +export default WorkspaceInvitationPage; diff --git a/web/components/account/auth-forms/auth-root.tsx b/web/components/account/auth-forms/auth-root.tsx index 66f32f9fd..9c89d488f 100644 --- a/web/components/account/auth-forms/auth-root.tsx +++ b/web/components/account/auth-forms/auth-root.tsx @@ -1,6 +1,6 @@ import React, { FC, useEffect, useState } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useRouter, useSearchParams } from "next/navigation"; import { IEmailCheckData } from "@plane/types"; // components import { @@ -35,7 +35,12 @@ type TAuthRoot = { export const AuthRoot: FC = observer((props) => { //router const router = useRouter(); - const { email: emailParam, invitation_id, slug: workspaceSlug, error_code } = router.query; + const searchParams = useSearchParams(); + // query params + const emailParam = searchParams.get("email"); + const invitation_id = searchParams.get("invitation_id"); + const workspaceSlug = searchParams.get("slug"); + const error_code = searchParams.get("error_code"); // props const { authMode: currentAuthMode } = props; // states @@ -130,7 +135,7 @@ export const AuthRoot: FC = observer((props) => { setErrorInfo(undefined); setEmail(""); setAuthStep(EAuthSteps.EMAIL); - router.push(currentAuthMode === EAuthModes.SIGN_IN ? `/` : "/sign-up", undefined, { shallow: true }); + router.push(currentAuthMode === EAuthModes.SIGN_IN ? `/` : "/sign-up"); }; // generating the unique code diff --git a/web/components/account/deactivate-account-modal.tsx b/web/components/account/deactivate-account-modal.tsx index ee5100e36..c2c6f20b1 100644 --- a/web/components/account/deactivate-account-modal.tsx +++ b/web/components/account/deactivate-account-modal.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import { Trash2 } from "lucide-react"; import { Dialog, Transition } from "@headlessui/react"; // hooks diff --git a/web/components/account/oauth/github-button.tsx b/web/components/account/oauth/github-button.tsx index 6d274c125..02b5f55bd 100644 --- a/web/components/account/oauth/github-button.tsx +++ b/web/components/account/oauth/github-button.tsx @@ -1,5 +1,5 @@ import { FC } from "react"; -import { useRouter } from "next/router"; +import { useSearchParams } from "next/navigation"; import Image from "next/image"; import { useTheme } from "next-themes"; // helpers @@ -13,8 +13,8 @@ export type GithubOAuthButtonProps = { }; export const GithubOAuthButton: FC = (props) => { - const { query } = useRouter(); - const { next_path } = query; + const searchParams = useSearchParams(); + const next_path = searchParams.get("next_path"); const { text } = props; // hooks const { resolvedTheme } = useTheme(); diff --git a/web/components/account/oauth/google-button.tsx b/web/components/account/oauth/google-button.tsx index 279c5ea42..c125589ac 100644 --- a/web/components/account/oauth/google-button.tsx +++ b/web/components/account/oauth/google-button.tsx @@ -1,5 +1,5 @@ import { FC } from "react"; -import { useRouter } from "next/router"; +import { useSearchParams } from "next/navigation"; import Image from "next/image"; import { useTheme } from "next-themes"; // helpers @@ -12,8 +12,8 @@ export type GoogleOAuthButtonProps = { }; export const GoogleOAuthButton: FC = (props) => { - const { query } = useRouter(); - const { next_path } = query; + const searchParams = useSearchParams(); + const next_path = searchParams.get("next_path"); const { text } = props; // hooks const { resolvedTheme } = useTheme(); diff --git a/web/components/analytics/custom-analytics/custom-analytics.tsx b/web/components/analytics/custom-analytics/custom-analytics.tsx index e5243cff5..413126c48 100644 --- a/web/components/analytics/custom-analytics/custom-analytics.tsx +++ b/web/components/analytics/custom-analytics/custom-analytics.tsx @@ -1,5 +1,5 @@ import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import { useForm } from "react-hook-form"; import useSWR from "swr"; import { IAnalyticsParams } from "@plane/types"; @@ -30,8 +30,7 @@ const analyticsService = new AnalyticsService(); export const CustomAnalytics: React.FC = observer((props) => { const { additionalParams, fullScreen } = props; - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId } = useParams(); const { control, watch, setValue } = useForm({ defaultValues }); diff --git a/web/components/analytics/custom-analytics/main-content.tsx b/web/components/analytics/custom-analytics/main-content.tsx index f57edbefd..b83f9cce2 100644 --- a/web/components/analytics/custom-analytics/main-content.tsx +++ b/web/components/analytics/custom-analytics/main-content.tsx @@ -1,4 +1,4 @@ -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import { mutate } from "swr"; import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types"; @@ -22,8 +22,7 @@ type Props = { export const CustomAnalyticsMainContent: React.FC = (props) => { const { analytics, error, fullScreen, params } = props; - const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug } = useParams(); const yAxisKey = params.y_axis === "issue_count" ? "count" : "estimate"; const barGraphData = convertResponseToBarGraphData(analytics?.distribution, params); diff --git a/web/components/analytics/custom-analytics/select/segment.tsx b/web/components/analytics/custom-analytics/select/segment.tsx index f9dedc965..842bcb623 100644 --- a/web/components/analytics/custom-analytics/select/segment.tsx +++ b/web/components/analytics/custom-analytics/select/segment.tsx @@ -1,4 +1,4 @@ -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import { IAnalyticsParams, TXAxisValues } from "@plane/types"; // ui @@ -12,8 +12,7 @@ type Props = { }; export const SelectSegment: React.FC = ({ value, onChange, params, analyticsOptions }) => { - const router = useRouter(); - const { cycleId, moduleId } = router.query; + const { cycleId, moduleId } = useParams(); return ( = (props) => { const { value, onChange, params, analyticsOptions } = props; - const router = useRouter(); - const { cycleId, moduleId } = router.query; + const { cycleId, moduleId } = useParams(); return ( { - const router = useRouter(); - const { projectId, cycleId, moduleId } = router.query; + const { projectId, cycleId, moduleId } = useParams(); const { getProjectById } = useProject(); const { getCycleById } = useCycle(); diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx index 93c91343c..876074b85 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import { mutate } from "swr"; // icons import { CalendarDays, Download, RefreshCw } from "lucide-react"; @@ -31,8 +31,7 @@ const analyticsService = new AnalyticsService(); export const CustomAnalyticsSidebar: React.FC = observer((props) => { const { analytics, params, isProjectLevel = false } = props; // router - const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const { workspaceSlug, projectId, cycleId, moduleId } = useParams(); // store hooks const { data: currentUser } = useUser(); const { workspaceProjectIds, getProjectById } = useProject(); diff --git a/web/components/analytics/scope-and-demand/scope-and-demand.tsx b/web/components/analytics/scope-and-demand/scope-and-demand.tsx index ede3a8e25..047553804 100644 --- a/web/components/analytics/scope-and-demand/scope-and-demand.tsx +++ b/web/components/analytics/scope-and-demand/scope-and-demand.tsx @@ -1,4 +1,4 @@ -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import useSWR from "swr"; @@ -21,8 +21,7 @@ const analyticsService = new AnalyticsService(); export const ScopeAndDemand: React.FC = (props) => { const { fullScreen = true } = props; - const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const { workspaceSlug, projectId, cycleId, moduleId } = useParams(); const isProjectLevel = projectId ? true : false; diff --git a/web/components/api-token/delete-token-modal.tsx b/web/components/api-token/delete-token-modal.tsx index 6918df463..2075e2e6e 100644 --- a/web/components/api-token/delete-token-modal.tsx +++ b/web/components/api-token/delete-token-modal.tsx @@ -1,5 +1,5 @@ import { useState, FC } from "react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import { mutate } from "swr"; // types import { IApiToken } from "@plane/types"; @@ -24,9 +24,8 @@ export const DeleteApiTokenModal: FC = (props) => { const { isOpen, onClose, tokenId } = props; // states const [deleteLoading, setDeleteLoading] = useState(false); - // router - const router = useRouter(); - const { workspaceSlug } = router.query; + // router params + const { workspaceSlug } = useParams(); const handleClose = () => { onClose(); diff --git a/web/components/api-token/modal/create-token-modal.tsx b/web/components/api-token/modal/create-token-modal.tsx index f71b5fcdd..1b5bf63ca 100644 --- a/web/components/api-token/modal/create-token-modal.tsx +++ b/web/components/api-token/modal/create-token-modal.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import { mutate } from "swr"; // types import { IApiToken } from "@plane/types"; @@ -30,8 +30,7 @@ export const CreateApiTokenModal: React.FC = (props) => { const [neverExpires, setNeverExpires] = useState(false); const [generatedToken, setGeneratedToken] = useState(null); // router - const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug } = useParams(); const handleClose = () => { onClose(); diff --git a/web/components/archives/archive-tabs-list.tsx b/web/components/archives/archive-tabs-list.tsx index 57d1c36a1..8088d745b 100644 --- a/web/components/archives/archive-tabs-list.tsx +++ b/web/components/archives/archive-tabs-list.tsx @@ -1,7 +1,7 @@ import { FC } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -import { useRouter } from "next/router"; +import { useParams, usePathname } from "next/navigation"; // constants import { ARCHIVES_TAB_LIST } from "@/constants/archives"; // hooks @@ -9,9 +9,9 @@ import { useProject } from "@/hooks/store"; export const ArchiveTabsList: FC = observer(() => { // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - const activeTab = router.pathname.split("/").pop(); + const { workspaceSlug, projectId } = useParams(); + const pathname = usePathname(); + const activeTab = pathname.split("/").pop(); // store hooks const { getProjectById } = useProject(); diff --git a/web/components/auth-screens/not-authorized-view.tsx b/web/components/auth-screens/not-authorized-view.tsx index 68d3def3b..10a04f81d 100644 --- a/web/components/auth-screens/not-authorized-view.tsx +++ b/web/components/auth-screens/not-authorized-view.tsx @@ -2,7 +2,7 @@ import React from "react"; import { observer } from "mobx-react"; import Image from "next/image"; import Link from "next/link"; -import { useRouter } from "next/router"; +import { useSearchParams } from "next/navigation"; // hooks import { useUser } from "@/hooks/store"; // layouts @@ -19,8 +19,8 @@ type Props = { export const NotAuthorizedView: React.FC = observer((props) => { const { actionButton, type } = props; // router - const { query } = useRouter(); - const { next_path } = query; + const searchParams = useSearchParams(); + const next_path = searchParams.get("next_path"); // hooks const { data: currentUser } = useUser(); diff --git a/web/components/auth-screens/project/join-project.tsx b/web/components/auth-screens/project/join-project.tsx index 0c5d4740a..8065c0708 100644 --- a/web/components/auth-screens/project/join-project.tsx +++ b/web/components/auth-screens/project/join-project.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import Image from "next/image"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; // hooks import { ClipboardList } from "lucide-react"; import { Button } from "@plane/ui"; @@ -19,8 +19,7 @@ export const JoinProject: React.FC = () => { } = useUser(); const { fetchProjects } = useProject(); - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId } = useParams(); const handleJoin = () => { if (!workspaceSlug || !projectId) return; diff --git a/web/components/automation/select-month-modal.tsx b/web/components/automation/select-month-modal.tsx index 7b068fa41..fc40cc31a 100644 --- a/web/components/automation/select-month-modal.tsx +++ b/web/components/automation/select-month-modal.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; // react-hook-form import { Controller, useForm } from "react-hook-form"; // headless ui @@ -19,8 +19,7 @@ type Props = { }; export const SelectMonthModal: React.FC = ({ type, initialValues, isOpen, handleClose, handleChange }) => { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId } = useParams(); const { formState: { errors, isSubmitting }, diff --git a/web/components/command-palette/actions/issue-actions/actions-list.tsx b/web/components/command-palette/actions/issue-actions/actions-list.tsx index 040eb2e3c..6c1c01aca 100644 --- a/web/components/command-palette/actions/issue-actions/actions-list.tsx +++ b/web/components/command-palette/actions/issue-actions/actions-list.tsx @@ -1,6 +1,8 @@ +"use client"; + import { Command } from "cmdk"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2, Users } from "lucide-react"; import { TIssue } from "@plane/types"; // hooks @@ -24,8 +26,7 @@ type Props = { export const CommandPaletteIssueActions: React.FC = observer((props) => { const { closePalette, issueDetails, pages, setPages, setPlaceholder, setSearchTerm } = props; // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId, issueId } = useParams(); // hooks const { issues: { updateIssue }, @@ -60,7 +61,7 @@ export const CommandPaletteIssueActions: React.FC = observer((props) => { }; const copyIssueUrlToClipboard = () => { - if (!router.query.issueId) return; + if (!issueId) return; const url = new URL(window.location.href); copyTextToClipboard(url.href) diff --git a/web/components/command-palette/actions/issue-actions/change-assignee.tsx b/web/components/command-palette/actions/issue-actions/change-assignee.tsx index aee7eceaf..30ef22aaf 100644 --- a/web/components/command-palette/actions/issue-actions/change-assignee.tsx +++ b/web/components/command-palette/actions/issue-actions/change-assignee.tsx @@ -1,6 +1,8 @@ +"use client"; + import { Command } from "cmdk"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import { Check } from "lucide-react"; import { TIssue } from "@plane/types"; // mobx store @@ -17,9 +19,8 @@ type Props = { export const ChangeIssueAssignee: React.FC = observer((props) => { const { closePalette, issue } = props; - // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + // router params + const { workspaceSlug, projectId } = useParams(); // store const { issues: { updateIssue }, diff --git a/web/components/command-palette/actions/issue-actions/change-priority.tsx b/web/components/command-palette/actions/issue-actions/change-priority.tsx index c154cc44c..5bd8ce850 100644 --- a/web/components/command-palette/actions/issue-actions/change-priority.tsx +++ b/web/components/command-palette/actions/issue-actions/change-priority.tsx @@ -1,6 +1,8 @@ +"use client"; `` + import { Command } from "cmdk"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import { Check } from "lucide-react"; import { TIssue, TIssuePriorities } from "@plane/types"; // mobx store @@ -18,10 +20,8 @@ type Props = { export const ChangeIssuePriority: React.FC = observer((props) => { const { closePalette, issue } = props; - - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - + // router params + const { workspaceSlug, projectId } = useParams(); const { issues: { updateIssue }, } = useIssues(EIssuesStoreType.PROJECT); diff --git a/web/components/command-palette/actions/issue-actions/change-state.tsx b/web/components/command-palette/actions/issue-actions/change-state.tsx index fce081dfb..5d512f4ac 100644 --- a/web/components/command-palette/actions/issue-actions/change-state.tsx +++ b/web/components/command-palette/actions/issue-actions/change-state.tsx @@ -1,6 +1,8 @@ +"use client"; + import { Command } from "cmdk"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; // hooks import { Check } from "lucide-react"; import { TIssue } from "@plane/types"; @@ -18,9 +20,8 @@ type Props = { export const ChangeIssueState: React.FC = observer((props) => { const { closePalette, issue } = props; - // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + // router params + const { workspaceSlug, projectId } = useParams(); // store hooks const { issues: { updateIssue }, diff --git a/web/components/command-palette/actions/search-results.tsx b/web/components/command-palette/actions/search-results.tsx index 489794295..6d779d654 100644 --- a/web/components/command-palette/actions/search-results.tsx +++ b/web/components/command-palette/actions/search-results.tsx @@ -1,5 +1,7 @@ +"use client"; + import { Command } from "cmdk"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import { IWorkspaceSearchResults } from "@plane/types"; // helpers import { commandGroups } from "@/components/command-palette"; diff --git a/web/components/command-palette/actions/theme-actions.tsx b/web/components/command-palette/actions/theme-actions.tsx index 7b0ddc72b..254dda42c 100644 --- a/web/components/command-palette/actions/theme-actions.tsx +++ b/web/components/command-palette/actions/theme-actions.tsx @@ -1,3 +1,5 @@ +"use client"; + import React, { FC, useEffect, useState } from "react"; import { Command } from "cmdk"; import { observer } from "mobx-react"; diff --git a/web/components/command-palette/actions/workspace-settings-actions.tsx b/web/components/command-palette/actions/workspace-settings-actions.tsx index 56c118a51..98a0a720b 100644 --- a/web/components/command-palette/actions/workspace-settings-actions.tsx +++ b/web/components/command-palette/actions/workspace-settings-actions.tsx @@ -1,7 +1,9 @@ +"use client"; + import { Command } from "cmdk"; // hooks import Link from "next/link"; -import { useRouter } from "next/router"; +import { useParams, useRouter } from "next/navigation"; // constants import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_LINKS } from "@/constants/workspace"; import { useUser } from "@/hooks/store"; @@ -14,7 +16,8 @@ export const CommandPaletteWorkspaceSettingsActions: React.FC = (props) = const { closePalette } = props; // router const router = useRouter(); - const { workspaceSlug } = router.query; + // router params + const { workspaceSlug } = useParams(); // mobx store const { membership: { currentWorkspaceRole }, diff --git a/web/components/command-palette/command-modal.tsx b/web/components/command-palette/command-modal.tsx index 6c05c9b17..e00af3d30 100644 --- a/web/components/command-palette/command-modal.tsx +++ b/web/components/command-palette/command-modal.tsx @@ -1,7 +1,9 @@ +"use client"; + import React, { useEffect, useState } from "react"; import { Command } from "cmdk"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams, useRouter } from "next/navigation"; import useSWR from "swr"; import { FolderPlus, Search, Settings } from "lucide-react"; import { Dialog, Transition } from "@headlessui/react"; @@ -67,7 +69,8 @@ export const CommandModal: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; + // router params + const { workspaceSlug, projectId, issueId } = useParams(); const page = pages[pages.length - 1]; diff --git a/web/components/command-palette/command-palette.tsx b/web/components/command-palette/command-palette.tsx index 41afbb5e2..4fe2e4438 100644 --- a/web/components/command-palette/command-palette.tsx +++ b/web/components/command-palette/command-palette.tsx @@ -1,6 +1,8 @@ +"use client"; + import React, { useCallback, useEffect, FC, useMemo } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams, usePathname, useRouter } from "next/navigation"; import useSWR from "swr"; // ui import { TOAST_TYPE, setToast } from "@plane/ui"; @@ -32,7 +34,10 @@ const issueService = new IssueService(); export const CommandPalette: FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, projectId, issueId, cycleId, moduleId } = router.query; + // router params + const { workspaceSlug, projectId, issueId, cycleId, moduleId } = useParams(); + // pathname + const pathname = usePathname(); // store hooks const { toggleSidebar } = useAppTheme(); const { setTrackElement } = useEventTracker(); @@ -260,7 +265,7 @@ export const CommandPalette: FC = observer(() => { return () => document.removeEventListener("keydown", handleKeyDown); }, [handleKeyDown]); - const isDraftIssue = router?.asPath?.includes("draft-issues") || false; + const isDraftIssue = pathname?.includes("draft-issues") || false; if (!currentUser) return null; diff --git a/web/components/command-palette/shortcuts-modal/modal.tsx b/web/components/command-palette/shortcuts-modal/modal.tsx index bbaa55464..f4076db4c 100644 --- a/web/components/command-palette/shortcuts-modal/modal.tsx +++ b/web/components/command-palette/shortcuts-modal/modal.tsx @@ -1,3 +1,5 @@ +"use client"; + import { FC, useState, Fragment } from "react"; import { Search, X } from "lucide-react"; import { Dialog, Transition } from "@headlessui/react"; diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index 5def2d7a9..27ac7bcea 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; // store hooks // icons import { @@ -29,8 +29,8 @@ import { usePlatformOS } from "@/hooks/use-platform-os"; // types export const IssueLink = ({ activity }: { activity: IIssueActivity }) => { - const router = useRouter(); - const { workspaceSlug } = router.query; + // router params + const { workspaceSlug } = useParams(); const { isMobile } = usePlatformOS(); return ( @@ -61,8 +61,8 @@ export const IssueLink = ({ activity }: { activity: IIssueActivity }) => { }; const UserLink = ({ activity }: { activity: IIssueActivity }) => { - const router = useRouter(); - const { workspaceSlug } = router.query; + // router params + const { workspaceSlug } = useParams(); return ( { - const router = useRouter(); - const { workspaceSlug } = router.query; + // router params + const { workspaceSlug } = useParams(); return ( <> diff --git a/web/components/core/image-picker-popover.tsx b/web/components/core/image-picker-popover.tsx index 4a080fb70..c4e692f73 100644 --- a/web/components/core/image-picker-popover.tsx +++ b/web/components/core/image-picker-popover.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState, useRef, useCallback } from "react"; import { observer } from "mobx-react"; import Image from "next/image"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import { useDropzone } from "react-dropzone"; import { Control, Controller } from "react-hook-form"; import useSWR from "swr"; @@ -58,9 +58,8 @@ export const ImagePickerPopover: React.FC = observer((props) => { }); // refs const ref = useRef(null); - // router - const router = useRouter(); - const { workspaceSlug } = router.query; + // router params + const { workspaceSlug } = useParams(); // store hooks const { config } = useInstance(); const { currentWorkspace } = useWorkspace(); diff --git a/web/components/core/list/list-item.tsx b/web/components/core/list/list-item.tsx index 8527d56b5..fba22bc91 100644 --- a/web/components/core/list/list-item.tsx +++ b/web/components/core/list/list-item.tsx @@ -1,5 +1,5 @@ import React, { FC } from "react"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; // ui import { ControlLink, Tooltip } from "@plane/ui"; // helpers diff --git a/web/components/core/modals/bulk-delete-issues-modal.tsx b/web/components/core/modals/bulk-delete-issues-modal.tsx index 22d9401cf..dd1b35307 100644 --- a/web/components/core/modals/bulk-delete-issues-modal.tsx +++ b/web/components/core/modals/bulk-delete-issues-modal.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import { SubmitHandler, useForm } from "react-hook-form"; import useSWR from "swr"; import { Search } from "lucide-react"; @@ -36,9 +36,8 @@ const issueService = new IssueService(); export const BulkDeleteIssuesModal: React.FC = observer((props) => { const { isOpen, onClose } = props; - // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + // router params + const { workspaceSlug, projectId } = useParams(); // hooks const { getProjectById } = useProject(); const { diff --git a/web/components/core/modals/gpt-assistant-popover.tsx b/web/components/core/modals/gpt-assistant-popover.tsx index 0c9c30b31..3f4680172 100644 --- a/web/components/core/modals/gpt-assistant-popover.tsx +++ b/web/components/core/modals/gpt-assistant-popover.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useRef, Fragment, Ref } from "react"; import { Placement } from "@popperjs/core"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import { Controller, useForm } from "react-hook-form"; // services import { usePopper } from "react-popper"; // ui @@ -42,8 +42,7 @@ export const GptAssistantPopover: React.FC = (props) => { const editorRef = useRef(null); const responseRef = useRef(null); // router - const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug } = useParams(); // popper const { styles, attributes } = usePopper(referenceElement, popperElement, { placement: placement ?? "auto", diff --git a/web/components/core/modals/workspace-image-upload-modal.tsx b/web/components/core/modals/workspace-image-upload-modal.tsx index 878217780..c9aef7f01 100644 --- a/web/components/core/modals/workspace-image-upload-modal.tsx +++ b/web/components/core/modals/workspace-image-upload-modal.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams, usePathname } from "next/navigation"; import { useDropzone } from "react-dropzone"; import { UserCircle2 } from "lucide-react"; import { Transition, Dialog } from "@headlessui/react"; @@ -31,8 +31,8 @@ export const WorkspaceImageUploadModal: React.FC = observer((props) => { const [image, setImage] = useState(null); const [isImageUploading, setIsImageUploading] = useState(false); // router - const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug } = useParams(); + const pathname = usePathname(); // store hooks const { config } = useInstance(); const { currentWorkspace } = useWorkspace(); @@ -55,7 +55,7 @@ export const WorkspaceImageUploadModal: React.FC = observer((props) => { }; const handleSubmit = async () => { - if (!image || (!workspaceSlug && router.pathname !== "/onboarding")) return; + if (!image || (!workspaceSlug && pathname !== "/onboarding")) return; setIsImageUploading(true); diff --git a/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx b/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx index 33370378e..fb847ec17 100644 --- a/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx +++ b/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx @@ -1,24 +1,18 @@ +"use client"; + import { FC } from "react"; import { observer } from "mobx-react"; import { Menu } from "lucide-react"; import { useAppTheme } from "@/hooks/store"; -type Props = { - onClick?: () => void; -}; - -export const SidebarHamburgerToggle: FC = observer((props) => { - const { onClick } = props; +export const SidebarHamburgerToggle: FC = observer(() => { // store hooks const { toggleSidebar } = useAppTheme(); return (
{ - if (onClick) onClick(); - else toggleSidebar(); - }} + onClick={() => toggleSidebar()} >
diff --git a/web/components/cycles/active-cycle/upcoming-cycles-list-item.tsx b/web/components/cycles/active-cycle/upcoming-cycles-list-item.tsx index a66af73c3..89d154cd2 100644 --- a/web/components/cycles/active-cycle/upcoming-cycles-list-item.tsx +++ b/web/components/cycles/active-cycle/upcoming-cycles-list-item.tsx @@ -1,7 +1,7 @@ import { useRef } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import { Users } from "lucide-react"; // ui import { Avatar, AvatarGroup, setPromiseToast } from "@plane/ui"; @@ -24,8 +24,7 @@ export const UpcomingCycleListItem: React.FC = observer((props) => { // refs const parentRef = useRef(null); // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId } = useParams(); // store hooks const { captureEvent } = useEventTracker(); const { addCycleToFavorites, getCycleById, removeCycleFromFavorites } = useCycle(); diff --git a/web/components/cycles/archived-cycles/header.tsx b/web/components/cycles/archived-cycles/header.tsx index e45b50baa..950128848 100644 --- a/web/components/cycles/archived-cycles/header.tsx +++ b/web/components/cycles/archived-cycles/header.tsx @@ -1,6 +1,6 @@ import { FC, useCallback, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; // icons import { ListFilter, Search, X } from "lucide-react"; // types @@ -18,8 +18,7 @@ import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; export const ArchivedCyclesHeader: FC = observer(() => { // router - const router = useRouter(); - const { projectId } = router.query; + const { projectId } = useParams(); // refs const inputRef = useRef(null); // hooks diff --git a/web/components/cycles/archived-cycles/modal.tsx b/web/components/cycles/archived-cycles/modal.tsx index b613d0de9..d4c2b0930 100644 --- a/web/components/cycles/archived-cycles/modal.tsx +++ b/web/components/cycles/archived-cycles/modal.tsx @@ -1,5 +1,5 @@ import { useState, Fragment } from "react"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import { Dialog, Transition } from "@headlessui/react"; // ui import { Button, TOAST_TYPE, setToast } from "@plane/ui"; diff --git a/web/components/cycles/archived-cycles/root.tsx b/web/components/cycles/archived-cycles/root.tsx index 4d47c8f34..bb175fe0e 100644 --- a/web/components/cycles/archived-cycles/root.tsx +++ b/web/components/cycles/archived-cycles/root.tsx @@ -1,6 +1,6 @@ import React from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import useSWR from "swr"; // types import { TCycleFilters } from "@plane/types"; @@ -17,8 +17,7 @@ import { useCycle, useCycleFilter } from "@/hooks/store"; export const ArchivedCycleLayoutRoot: React.FC = observer(() => { // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId } = useParams(); // hooks const { fetchArchivedCycles, currentProjectArchivedCycleIds, loader } = useCycle(); // cycle filters hook diff --git a/web/components/cycles/board/cycles-board-card.tsx b/web/components/cycles/board/cycles-board-card.tsx index 641007798..700c6a882 100644 --- a/web/components/cycles/board/cycles-board-card.tsx +++ b/web/components/cycles/board/cycles-board-card.tsx @@ -1,7 +1,7 @@ import { FC, MouseEvent, useRef } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -import { useRouter } from "next/router"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { CalendarCheck2, CalendarClock, Info, MoveRight } from "lucide-react"; // types import type { TCycleGroups } from "@plane/types"; @@ -16,6 +16,7 @@ import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker"; import { EUserWorkspaceRoles } from "@/constants/workspace"; // helpers import { findHowManyDaysLeft, getDate, renderFormattedDate } from "@/helpers/date-time.helper"; +import { generateQueryParams } from "@/helpers/router.helper"; // hooks import { useEventTracker, useCycle, useUser, useMember } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -32,6 +33,8 @@ export const CyclesBoardCard: FC = observer((props) => { const parentRef = useRef(null); // router const router = useRouter(); + const searchParams = useSearchParams(); + const pathname = usePathname(); // store const { captureEvent } = useEventTracker(); const { @@ -130,21 +133,14 @@ export const CyclesBoardCard: FC = observer((props) => { }; const openCycleOverview = (e: MouseEvent) => { - const { query } = router; e.preventDefault(); e.stopPropagation(); - if (query.peekCycle) { - delete query.peekCycle; - router.push({ - pathname: router.pathname, - query: { ...query }, - }); + const query = generateQueryParams(searchParams, ['peekCycle']); + if (searchParams.has('peekCycle')) { + router.push(`${pathname}?${query}`); } else { - router.push({ - pathname: router.pathname, - query: { ...query, peekCycle: cycleId }, - }); + router.push(`${pathname}?${query}&peekCycle=${cycleId}`); } }; diff --git a/web/components/cycles/cycle-peek-overview.tsx b/web/components/cycles/cycle-peek-overview.tsx index 316f4538c..3a5c1d9b0 100644 --- a/web/components/cycles/cycle-peek-overview.tsx +++ b/web/components/cycles/cycle-peek-overview.tsx @@ -1,7 +1,8 @@ import React, { useEffect } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; // hooks +import { generateQueryParams } from "@/helpers/router.helper"; import { useCycle } from "@/hooks/store"; // components import { CycleDetailsSidebar } from "./sidebar"; @@ -15,18 +16,17 @@ type Props = { export const CyclePeekOverview: React.FC = observer(({ projectId, workspaceSlug, isArchived = false }) => { // router const router = useRouter(); - const { peekCycle } = router.query; + const pathname = usePathname(); + const searchParams = useSearchParams(); + const peekCycle = searchParams.get("peekCycle"); // refs const ref = React.useRef(null); // store hooks const { fetchCycleDetails, fetchArchivedCycleDetails } = useCycle(); const handleClose = () => { - delete router.query.peekCycle; - router.push({ - pathname: router.pathname, - query: { ...router.query }, - }); + const query = generateQueryParams(searchParams, ['peekCycle']); + router.push(`${pathname}?${query}`); }; useEffect(() => { diff --git a/web/components/cycles/delete-modal.tsx b/web/components/cycles/delete-modal.tsx index 9a6578f49..93407a236 100644 --- a/web/components/cycles/delete-modal.tsx +++ b/web/components/cycles/delete-modal.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams, useRouter, useSearchParams } from "next/navigation"; // types import { ICycle } from "@plane/types"; // ui @@ -29,7 +29,9 @@ export const CycleDeleteModal: React.FC = observer((props) => { const { deleteCycle } = useCycle(); // router const router = useRouter(); - const { cycleId, peekCycle } = router.query; + const { cycleId } = useParams(); + const searchParams = useSearchParams(); + const peekCycle = searchParams.get("peekCycle"); const formSubmit = async () => { if (!cycle) return; diff --git a/web/components/cycles/gantt-chart/blocks.tsx b/web/components/cycles/gantt-chart/blocks.tsx index 68e25a43c..d0c7be800 100644 --- a/web/components/cycles/gantt-chart/blocks.tsx +++ b/web/components/cycles/gantt-chart/blocks.tsx @@ -1,6 +1,6 @@ import { observer } from "mobx-react"; import Link from "next/link"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; // hooks // ui import { Tooltip, ContrastIcon } from "@plane/ui"; diff --git a/web/components/cycles/gantt-chart/cycles-list-layout.tsx b/web/components/cycles/gantt-chart/cycles-list-layout.tsx index 1b0e0ab82..aa0aeea41 100644 --- a/web/components/cycles/gantt-chart/cycles-list-layout.tsx +++ b/web/components/cycles/gantt-chart/cycles-list-layout.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import { ICycle } from "@plane/types"; // hooks import { CycleGanttBlock } from "@/components/cycles"; @@ -19,8 +19,7 @@ type Props = { export const CyclesListGanttChartView: FC = observer((props) => { const { cycleIds } = props; // router - const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug } = useParams(); // store hooks const { getCycleById, updateCycleDetails } = useCycle(); diff --git a/web/components/cycles/list/cycles-list-item.tsx b/web/components/cycles/list/cycles-list-item.tsx index 414c8081a..78a1cfb7b 100644 --- a/web/components/cycles/list/cycles-list-item.tsx +++ b/web/components/cycles/list/cycles-list-item.tsx @@ -1,6 +1,6 @@ import { FC, MouseEvent, useRef } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; // icons import { Check, Info } from "lucide-react"; // types @@ -10,6 +10,8 @@ import { CircularProgressIndicator } from "@plane/ui"; // components import { ListItem } from "@/components/core/list"; import { CycleListItemAction } from "@/components/cycles/list"; +// helpers +import { generateQueryParams } from "@/helpers/router.helper"; // hooks import { useCycle } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -31,6 +33,8 @@ export const CyclesListItem: FC = observer((props) => { const parentRef = useRef(null); // router const router = useRouter(); + const searchParams = useSearchParams(); + const pathname = usePathname(); // hooks const { isMobile } = usePlatformOS(); // store hooks @@ -59,21 +63,14 @@ export const CyclesListItem: FC = observer((props) => { // handlers const openCycleOverview = (e: MouseEvent) => { - const { query } = router; e.preventDefault(); e.stopPropagation(); - if (query.peekCycle) { - delete query.peekCycle; - router.push({ - pathname: router.pathname, - query: { ...query }, - }); + const query = generateQueryParams(searchParams, ["peekCycle"]); + if (searchParams.has("peekCycle")) { + router.push(`${pathname}?${query}`); } else { - router.push({ - pathname: router.pathname, - query: { ...query, peekCycle: cycleId }, - }); + router.push(`${pathname}?${query}&peekCycle=${cycleId}`); } }; diff --git a/web/components/cycles/quick-actions.tsx b/web/components/cycles/quick-actions.tsx index 9c130ef7a..084395956 100644 --- a/web/components/cycles/quick-actions.tsx +++ b/web/components/cycles/quick-actions.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; // icons import { ArchiveRestoreIcon, ExternalLink, LinkIcon, Pencil, Trash2 } from "lucide-react"; // ui diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 595fe9b7a..cc40b8157 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from "react"; import isEmpty from "lodash/isEmpty"; import isEqual from "lodash/isEqual"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams, useRouter, useSearchParams } from "next/navigation"; import { Controller, useForm } from "react-hook-form"; // icons import { @@ -60,7 +60,9 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { const [cycleDeleteModal, setCycleDeleteModal] = useState(false); // router const router = useRouter(); - const { workspaceSlug, projectId, peekCycle } = router.query; + const { workspaceSlug, projectId } = useParams(); + const searchParams = useSearchParams(); + const peekCycle = searchParams.get("peekCycle"); // store hooks const { setTrackElement, captureCycleEvent } = useEventTracker(); const { diff --git a/web/components/cycles/transfer-issues-modal.tsx b/web/components/cycles/transfer-issues-modal.tsx index 399774faa..5d547bcfd 100644 --- a/web/components/cycles/transfer-issues-modal.tsx +++ b/web/components/cycles/transfer-issues-modal.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import { AlertCircle, Search, X } from "lucide-react"; import { Dialog, Transition } from "@headlessui/react"; // hooks @@ -28,8 +28,7 @@ export const TransferIssuesModal: React.FC = observer((props) => { issues: { transferIssuesFromCycle }, } = useIssues(EIssuesStoreType.CYCLE); - const router = useRouter(); - const { workspaceSlug, projectId, cycleId } = router.query; + const { workspaceSlug, projectId, cycleId } = useParams(); const transferIssue = async (payload: any) => { if (!workspaceSlug || !projectId || !cycleId) return; diff --git a/web/components/cycles/transfer-issues.tsx b/web/components/cycles/transfer-issues.tsx index d7a5a2178..881b00175 100644 --- a/web/components/cycles/transfer-issues.tsx +++ b/web/components/cycles/transfer-issues.tsx @@ -1,7 +1,7 @@ import React from "react"; import isEmpty from "lodash/isEmpty"; -import { useRouter } from "next/router"; +import { useParams } from "next/navigation"; import useSWR from "swr"; @@ -24,8 +24,7 @@ const cycleService = new CycleService(); export const TransferIssues: React.FC = (props) => { const { handleClick, disabled = false } = props; - const router = useRouter(); - const { workspaceSlug, projectId, cycleId } = router.query; + const { workspaceSlug, projectId, cycleId } = useParams(); const { data: cycleDetails } = useSWR( cycleId ? CYCLE_DETAILS(cycleId as string) : null, diff --git a/web/components/dashboard/widgets/issues-by-priority.tsx b/web/components/dashboard/widgets/issues-by-priority.tsx index 9862b440b..34bcae240 100644 --- a/web/components/dashboard/widgets/issues-by-priority.tsx +++ b/web/components/dashboard/widgets/issues-by-priority.tsx @@ -1,7 +1,7 @@ import { useEffect } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import { TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; // hooks // components diff --git a/web/components/dashboard/widgets/issues-by-state-group.tsx b/web/components/dashboard/widgets/issues-by-state-group.tsx index 22552ea8c..bfdc7d669 100644 --- a/web/components/dashboard/widgets/issues-by-state-group.tsx +++ b/web/components/dashboard/widgets/issues-by-state-group.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import { TIssuesByStateGroupsWidgetFilters, TIssuesByStateGroupsWidgetResponse, TStateGroups } from "@plane/types"; // hooks import { diff --git a/web/components/estimates/create-update-estimate-modal.tsx b/web/components/estimates/create-update-estimate-modal.tsx new file mode 100644 index 000000000..5e68095f3 --- /dev/null +++ b/web/components/estimates/create-update-estimate-modal.tsx @@ -0,0 +1,291 @@ +import React, { useEffect } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { Controller, useForm } from "react-hook-form"; +// types +import { IEstimate, IEstimateFormData } from "@plane/types"; +// ui +import { Button, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { EModalPosition, EModalWidth, ModalCore } from "@/components/core"; +// helpers +import { checkDuplicates } from "@/helpers/array.helper"; +// hooks +import { useEstimate } from "@/hooks/store"; + +type Props = { + isOpen: boolean; + handleClose: () => void; + data?: IEstimate; +}; + +const defaultValues = { + name: "", + description: "", + value1: "", + value2: "", + value3: "", + value4: "", + value5: "", + value6: "", +}; + +type FormValues = typeof defaultValues; + +export const CreateUpdateEstimateModal: React.FC = observer((props) => { + const { handleClose, data, isOpen } = props; + // router + const { workspaceSlug, projectId } = useParams(); + // store hooks + const { createEstimate, updateEstimate } = useEstimate(); + // form info + const { + formState: { errors, isSubmitting }, + handleSubmit, + control, + reset, + } = useForm({ + defaultValues, + }); + + const onClose = () => { + handleClose(); + reset(); + }; + + const handleCreateEstimate = async (payload: IEstimateFormData) => { + if (!workspaceSlug || !projectId) return; + + await createEstimate(workspaceSlug.toString(), projectId.toString(), payload) + .then(() => { + onClose(); + }) + .catch((err) => { + const error = err?.error; + const errorString = Array.isArray(error) ? error[0] : error; + + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: + errorString ?? err.status === 400 + ? "Estimate with that name already exists. Please try again with another name." + : "Estimate could not be created. Please try again.", + }); + }); + }; + + const handleUpdateEstimate = async (payload: IEstimateFormData) => { + if (!workspaceSlug || !projectId || !data) return; + + await updateEstimate(workspaceSlug.toString(), projectId.toString(), data.id, payload) + .then(() => { + onClose(); + }) + .catch((err) => { + const error = err?.error; + const errorString = Array.isArray(error) ? error[0] : error; + + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: errorString ?? "Estimate could not be updated. Please try again.", + }); + }); + }; + + const onSubmit = async (formData: FormValues) => { + if (!formData.name || formData.name === "") { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Estimate title cannot be empty.", + }); + return; + } + + if ( + formData.value1 === "" || + formData.value2 === "" || + formData.value3 === "" || + formData.value4 === "" || + formData.value5 === "" || + formData.value6 === "" + ) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Estimate point cannot be empty.", + }); + return; + } + + if ( + formData.value1.length > 20 || + formData.value2.length > 20 || + formData.value3.length > 20 || + formData.value4.length > 20 || + formData.value5.length > 20 || + formData.value6.length > 20 + ) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Estimate point cannot have more than 20 characters.", + }); + return; + } + + if ( + checkDuplicates([ + formData.value1, + formData.value2, + formData.value3, + formData.value4, + formData.value5, + formData.value6, + ]) + ) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Estimate points cannot have duplicate values.", + }); + return; + } + + const payload: IEstimateFormData = { + estimate: { + name: formData.name, + description: formData.description, + }, + estimate_points: [], + }; + + for (let i = 0; i < 6; i++) { + const point = { + key: i, + value: formData[`value${i + 1}` as keyof FormValues], + }; + + if (data) + payload.estimate_points.push({ + id: data.points[i].id, + ...point, + }); + else payload.estimate_points.push({ ...point }); + } + + if (data) await handleUpdateEstimate(payload); + else await handleCreateEstimate(payload); + }; + + useEffect(() => { + if (data) + reset({ + ...defaultValues, + ...data, + value1: data.points[0]?.value, + value2: data.points[1]?.value, + value3: data.points[2]?.value, + value4: data.points[3]?.value, + value5: data.points[4]?.value, + value6: data.points[5]?.value, + }); + else reset({ ...defaultValues }); + }, [data, reset]); + + return ( + + +
+
{data ? "Update" : "Create"} Estimate
+
+
+ ( + + )} + /> +
+
+ ( +