From ed4aae47a28fe2279d2aa9222f82d3a7594c35e3 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Thu, 30 Mar 2023 17:04:41 +0530 Subject: [PATCH] style: my profile page (#608) --- apps/app/constants/fetch-keys.ts | 3 +- apps/app/layouts/app-layout/index.tsx | 12 +- apps/app/layouts/settings-navbar.tsx | 22 +- apps/app/pages/[workspaceSlug]/me/profile.tsx | 338 ------------------ .../[workspaceSlug]/me/profile/activity.tsx | 84 +++++ .../[workspaceSlug]/me/profile/index.tsx | 324 +++++++++++++++++ .../pages/[workspaceSlug]/settings/index.tsx | 6 +- apps/app/services/user.service.ts | 6 +- apps/app/types/users.d.ts | 33 ++ 9 files changed, 477 insertions(+), 351 deletions(-) delete mode 100644 apps/app/pages/[workspaceSlug]/me/profile.tsx create mode 100644 apps/app/pages/[workspaceSlug]/me/profile/activity.tsx create mode 100644 apps/app/pages/[workspaceSlug]/me/profile/index.tsx diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index 2110f36fd..6081b4d56 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -94,8 +94,7 @@ export const STATE_LIST = (projectId: string) => `STATE_LIST_${projectId.toUpper export const STATE_DETAIL = "STATE_DETAILS"; export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug.toUpperCase()}`; -export const USER_ACTIVITY = (workspaceSlug: string) => - `USER_ACTIVITY_${workspaceSlug.toUpperCase()}`; +export const USER_ACTIVITY = "USER_ACTIVITY"; export const USER_WORKSPACE_DASHBOARD = (workspaceSlug: string) => `USER_WORKSPACE_DASHBOARD_${workspaceSlug.toUpperCase()}`; export const USER_PROJECT_VIEW = (projectId: string) => diff --git a/apps/app/layouts/app-layout/index.tsx b/apps/app/layouts/app-layout/index.tsx index fa1a2d138..5de05c78f 100644 --- a/apps/app/layouts/app-layout/index.tsx +++ b/apps/app/layouts/app-layout/index.tsx @@ -42,6 +42,7 @@ type AppLayoutProps = { left?: JSX.Element; right?: JSX.Element; settingsLayout?: boolean; + profilePage?: boolean; memberType?: UserAuth; }; @@ -55,6 +56,7 @@ const AppLayout: FC = ({ left, right, settingsLayout = false, + profilePage = false, memberType, }) => { // states @@ -152,13 +154,17 @@ const AppLayout: FC = ({

- {projectId ? "Project" : "Workspace"} Settings + {profilePage ? "Profile" : projectId ? "Project" : "Workspace"} Settings

- This information will be displayed to every member of the project. + {profilePage + ? "This information will be visible to only you." + : projectId + ? "This information will be displayed to every member of the project." + : "This information will be displayed to every member of the workspace."}

- +
)} {children} diff --git a/apps/app/layouts/settings-navbar.tsx b/apps/app/layouts/settings-navbar.tsx index 48aaf0f14..0aa413892 100644 --- a/apps/app/layouts/settings-navbar.tsx +++ b/apps/app/layouts/settings-navbar.tsx @@ -1,7 +1,11 @@ import Link from "next/link"; import { useRouter } from "next/router"; -const SettingsNavbar: React.FC = () => { +type Props = { + profilePage: boolean; +}; + +const SettingsNavbar: React.FC = ({ profilePage = false }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -65,9 +69,23 @@ const SettingsNavbar: React.FC = () => { }, ]; + const profileLinks: Array<{ + label: string; + href: string; + }> = [ + { + label: "General", + href: `/${workspaceSlug}/me/profile`, + }, + { + label: "Activity", + href: `/${workspaceSlug}/me/profile/activity`, + }, + ]; + return (
- {(projectId ? projectLinks : workspaceLinks).map((link) => ( + {(profilePage ? profileLinks : projectId ? projectLinks : workspaceLinks).map((link) => (
= { - avatar: "", - first_name: "", - last_name: "", - email: "", -}; - -const Profile: NextPage = () => { - const [isEditing, setIsEditing] = useState(false); - const [isRemoving, setIsRemoving] = useState(false); - const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); - - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { - register, - handleSubmit, - reset, - watch, - setValue, - formState: { errors, isSubmitting }, - } = useForm({ defaultValues }); - - const { setToastAlert } = useToast(); - const { user: myProfile, mutateUser, assignedIssuesLength, workspaceInvitesLength } = useUser(); - - useEffect(() => { - reset({ ...defaultValues, ...myProfile }); - }, [myProfile, reset]); - - const onSubmit = (formData: IUser) => { - const payload: Partial = { - first_name: formData.first_name, - last_name: formData.last_name, - avatar: formData.avatar, - }; - userService - .updateUser(payload) - .then((res) => { - mutateUser((prevData) => { - if (!prevData) return prevData; - return { ...prevData, user: { ...payload, ...res } }; - }, false); - setIsEditing(false); - setToastAlert({ - type: "success", - title: "Success!", - message: "Profile updated successfully.", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "There was some error in updating your profile. Please try again.", - }); - }); - }; - - const handleDelete = (url: string | null | undefined, updateUser: boolean = false) => { - if (!url) return; - - setIsRemoving(true); - - const index = url.indexOf(".com"); - const asset = url.substring(index + 5); - - fileService.deleteUserFile(asset).then(() => { - if (updateUser) - userService - .updateUser({ avatar: "" }) - .then((res) => { - setIsRemoving(false); - setToastAlert({ - type: "success", - title: "Success!", - message: "Profile picture removed successfully.", - }); - mutateUser((prevData) => { - if (!prevData) return prevData; - return { ...prevData, user: res }; - }, false); - }) - .catch(() => { - setIsRemoving(false); - setToastAlert({ - type: "error", - title: "Error!", - message: "There was some error in deleting your profile picture. Please try again.", - }); - }); - }); - }; - - const quickLinks = [ - { - icon: RectangleStackIcon, - title: "Assigned Issues", - number: assignedIssuesLength, - description: "View issues assigned to you.", - href: `/${workspaceSlug}/me/my-issues`, - }, - { - icon: UserPlusIcon, - title: "Workspace Invitations", - number: workspaceInvitesLength, - description: "View your workspace invitations.", - href: "/invitations", - }, - ]; - - return ( - - - - } - > - setIsImageUploadModalOpen(false)} - onSuccess={(url) => { - handleDelete(myProfile?.avatar); - setValue("avatar", url); - handleSubmit(onSubmit)(); - setIsImageUploadModalOpen(false); - }} - value={watch("avatar") !== "" ? watch("avatar") : undefined} - userImage - /> -
- {myProfile ? ( - <> -
-
- -
-
-
-
- {!watch("avatar") || watch("avatar") === "" ? ( - - ) : ( -
- {myProfile.first_name} setIsImageUploadModalOpen(true)} - /> -
- )} -
-
-

- Max file size is 5MB. -
- Supported file types are .jpg and .png. -

-
- setIsImageUploadModalOpen(true)}> - Upload new - - {myProfile.avatar && myProfile.avatar !== "" && ( - handleDelete(myProfile.avatar, true)} - loading={isRemoving} - > - {isRemoving ? "Removing..." : "Remove"} - - )} -
-
-
-
-
-
-

First Name

- {isEditing ? ( - - ) : ( -

{myProfile.first_name}

- )} -
-
-

Last Name

- {isEditing ? ( - - ) : ( -

{myProfile.last_name}

- )} -
-
-

Email ID

-

{myProfile.email}

-
-
- {isEditing && ( -
- - {isSubmitting ? "Updating Profile..." : "Update Profile"} - -
- )} -
-
-
-

Quick Links

-
-
-
- - ) : ( -
- -
- )} -
- - ); -}; - -export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { - const user = await requiredAuth(ctx.req?.headers.cookie); - - const redirectAfterSignIn = ctx.resolvedUrl; - - if (!user) { - return { - redirect: { - destination: `/signin?next=${redirectAfterSignIn}`, - permanent: false, - }, - }; - } - - return { - props: { - user, - }, - }; -}; - -export default Profile; diff --git a/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx b/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx new file mode 100644 index 000000000..bcc596843 --- /dev/null +++ b/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx @@ -0,0 +1,84 @@ +import { GetServerSidePropsContext } from "next"; + +import useSWR from "swr"; + +// services +import userService from "services/user.service"; +// lib +import { requiredAuth } from "lib/auth"; +// layouts +import AppLayout from "layouts/app-layout"; +// ui +import { Loader } from "components/ui"; +import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; +// helpers +import { timeAgo } from "helpers/date-time.helper"; +// fetch-keys +import { USER_ACTIVITY } from "constants/fetch-keys"; + +const ProfileActivity = () => { + const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity()); + + return ( + + + + } + settingsLayout + profilePage + > + {userActivity ? ( +
+ {userActivity.results.length > 0 + ? userActivity.results.map((activity) => ( +
+

+ {activity.comment} +

+
{timeAgo(activity.created_at)}
+
+ )) + : null} +
+ ) : ( + + + + + + + )} +
+ ); +}; + +export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { + const user = await requiredAuth(ctx.req?.headers.cookie); + + const redirectAfterSignIn = ctx.resolvedUrl; + + if (!user) { + return { + redirect: { + destination: `/signin?next=${redirectAfterSignIn}`, + permanent: false, + }, + }; + } + + return { + props: { + user, + }, + }; +}; + +export default ProfileActivity; diff --git a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx new file mode 100644 index 000000000..af5487e41 --- /dev/null +++ b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx @@ -0,0 +1,324 @@ +import React, { useEffect, useState } from "react"; + +import Link from "next/link"; +import Image from "next/image"; + +// react-hook-form +import { useForm } from "react-hook-form"; +// lib +import { requiredAuth } from "lib/auth"; +// services +import fileService from "services/file.service"; +import userService from "services/user.service"; +// hooks +import useUser from "hooks/use-user"; +import useToast from "hooks/use-toast"; +// layouts +import AppLayout from "layouts/app-layout"; +// components +import { ImageUploadModal } from "components/core"; +// ui +import { DangerButton, Input, SecondaryButton, Spinner } from "components/ui"; +import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; +// icons +import { + ChevronRightIcon, + PencilIcon, + RectangleStackIcon, + UserIcon, + UserPlusIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; +// types +import type { NextPage, GetServerSidePropsContext } from "next"; +import type { IUser } from "types"; +import { useRouter } from "next/dist/client/router"; + +const defaultValues: Partial = { + avatar: "", + first_name: "", + last_name: "", + email: "", +}; + +const Profile: NextPage = () => { + const [isEditing, setIsEditing] = useState(false); + const [isRemoving, setIsRemoving] = useState(false); + const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { + register, + handleSubmit, + reset, + watch, + setValue, + formState: { errors, isSubmitting }, + } = useForm({ defaultValues }); + + const { setToastAlert } = useToast(); + const { user: myProfile, mutateUser, assignedIssuesLength, workspaceInvitesLength } = useUser(); + + useEffect(() => { + reset({ ...defaultValues, ...myProfile }); + }, [myProfile, reset]); + + const onSubmit = async (formData: IUser) => { + const payload: Partial = { + first_name: formData.first_name, + last_name: formData.last_name, + avatar: formData.avatar, + }; + + await userService + .updateUser(payload) + .then((res) => { + mutateUser((prevData) => { + if (!prevData) return prevData; + return { ...prevData, user: { ...payload, ...res } }; + }, false); + setIsEditing(false); + setToastAlert({ + type: "success", + title: "Success!", + message: "Profile updated successfully.", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "There was some error in updating your profile. Please try again.", + }); + }); + }; + + const handleDelete = (url: string | null | undefined, updateUser: boolean = false) => { + if (!url) return; + + setIsRemoving(true); + + const index = url.indexOf(".com"); + const asset = url.substring(index + 5); + + fileService.deleteUserFile(asset).then(() => { + if (updateUser) + userService + .updateUser({ avatar: "" }) + .then((res) => { + setIsRemoving(false); + setToastAlert({ + type: "success", + title: "Success!", + message: "Profile picture removed successfully.", + }); + mutateUser((prevData) => { + if (!prevData) return prevData; + return { ...prevData, user: res }; + }, false); + }) + .catch(() => { + setIsRemoving(false); + setToastAlert({ + type: "error", + title: "Error!", + message: "There was some error in deleting your profile picture. Please try again.", + }); + }); + }); + }; + + const quickLinks = [ + { + icon: RectangleStackIcon, + title: "Assigned Issues", + number: assignedIssuesLength, + description: "View issues assigned to you.", + href: `/${workspaceSlug}/me/my-issues`, + }, + { + icon: UserPlusIcon, + title: "Workspace Invitations", + number: workspaceInvitesLength, + description: "View your workspace invitations.", + href: "/invitations", + }, + ]; + + return ( + + + + } + settingsLayout + profilePage + > + setIsImageUploadModalOpen(false)} + onSuccess={(url) => { + handleDelete(myProfile?.avatar); + setValue("avatar", url); + handleSubmit(onSubmit)(); + setIsImageUploadModalOpen(false); + }} + value={watch("avatar") !== "" ? watch("avatar") : undefined} + userImage + /> + {myProfile ? ( +
+
+
+

Profile Picture

+

+ Max file size is 5MB. Supported file types are .jpg and .png. +

+
+
+
+ +
+ { + setIsImageUploadModalOpen(true); + }} + > + Upload + + {myProfile.avatar && myProfile.avatar !== "" && ( + handleDelete(myProfile.avatar, true)} + loading={isRemoving} + > + {isRemoving ? "Removing..." : "Remove"} + + )} +
+
+
+
+
+
+

Full Name

+

+ This name will be reflected on all the projects you are working on. +

+
+
+ + +
+
+
+
+

Email

+

The email address that you are using.

+
+
+ +
+
+
+
+

Role

+

The email address that you are using.

+
+
+ +
+
+
+ + {isSubmitting ? "Updating..." : "Update profile"} + +
+
+ ) : ( +
+ +
+ )} +
+ ); +}; + +export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { + const user = await requiredAuth(ctx.req?.headers.cookie); + + const redirectAfterSignIn = ctx.resolvedUrl; + + if (!user) { + return { + redirect: { + destination: `/signin?next=${redirectAfterSignIn}`, + permanent: false, + }, + }; + } + + return { + props: { + user, + }, + }; +}; + +export default Profile; diff --git a/apps/app/pages/[workspaceSlug]/settings/index.tsx b/apps/app/pages/[workspaceSlug]/settings/index.tsx index 7825ec234..2694af082 100644 --- a/apps/app/pages/[workspaceSlug]/settings/index.tsx +++ b/apps/app/pages/[workspaceSlug]/settings/index.tsx @@ -47,9 +47,9 @@ const WorkspaceSettings: NextPage = (props) => { const [isImageUploading, setIsImageUploading] = useState(false); const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); - const { - query: { workspaceSlug }, - } = useRouter(); + const router = useRouter(); + const { workspaceSlug } = router.query; + const { setToastAlert } = useToast(); const { data: activeWorkspace } = useSWR( diff --git a/apps/app/services/user.service.ts b/apps/app/services/user.service.ts index c5748cf61..95f42554d 100644 --- a/apps/app/services/user.service.ts +++ b/apps/app/services/user.service.ts @@ -2,7 +2,7 @@ import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; -import type { IUser, IUserActivity, IUserWorkspaceDashboard } from "types"; +import type { IUser, IUserActivityResponse, IUserWorkspaceDashboard } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -58,8 +58,8 @@ class UserService extends APIService { }); } - async userActivity(workspaceSlug: string): Promise { - return this.get(`/api/users/me/workspaces/${workspaceSlug}/activity-graph/`) + async getUserActivity(): Promise { + return this.get("/api/users/activities/") .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/apps/app/types/users.d.ts b/apps/app/types/users.d.ts index 1758a9ce2..15c84aaaf 100644 --- a/apps/app/types/users.d.ts +++ b/apps/app/types/users.d.ts @@ -61,6 +61,39 @@ export interface IUserWorkspaceDashboard { upcoming_issues: IIssueLite[]; } +export interface IUserDetailedActivity { + actor: string; + actor_detail: IUserLite; + attachments: any[]; + comment: string; + created_at: string; + created_by: string | null; + field: string; + id: string; + issue: string; + issue_comment: string | null; + new_identifier: string | null; + new_value: string | null; + old_identifier: string | null; + old_value: string | null; + project: string; + updated_at: string; + updated_by: string | null; + verb: string; + workspace: string; +} + +export interface IUserActivityResponse { + count: number; + extra_stats: null; + next_cursor: string; + next_page_results: boolean; + prev_cursor: string; + prev_page_results: boolean; + results: IUserDetailedActivity[]; + total_pages: number; +} + export type UserAuth = { isMember: boolean; isOwner: boolean;