From 13985df8601f8aca2395a42385675cada31d2a5c Mon Sep 17 00:00:00 2001 From: Dakshesh Jain Date: Mon, 19 Dec 2022 20:13:43 +0530 Subject: [PATCH] feat: made emoji-icon-picker fix: google prompt coming up after leaving sign in, refractor: saving views data to db instead of local-storage --- .../project/create-project-modal.tsx | 22 +- .../project/settings/GeneralSettings.tsx | 40 +- apps/app/components/sidebar/projects-list.tsx | 13 +- .../components/socialbuttons/google-login.tsx | 11 +- apps/app/constants/api-routes.ts | 2 + apps/app/contexts/theme.context.tsx | 60 +- apps/app/lib/hooks/useIssuesFilter.tsx | 1 - apps/app/lib/hooks/useIssuesProperties.tsx | 1 + apps/app/lib/hooks/useMyIssueFilter.tsx | 1 + .../app/lib/hooks/useOutsideClickDetector.tsx | 19 + apps/app/lib/services/project.service.ts | 11 + .../projects/[projectId]/issues/index.tsx | 2 +- .../pages/projects/[projectId]/settings.tsx | 2 + apps/app/types/issues.d.ts | 1 + apps/app/types/projects.d.ts | 1 + apps/app/ui/emoji-icon-picker/emojis.json | 1090 +++++++++++++++++ apps/app/ui/emoji-icon-picker/helpers.ts | 26 + apps/app/ui/emoji-icon-picker/index.tsx | 125 ++ apps/app/ui/emoji-icon-picker/types.d.ts | 5 + apps/app/ui/index.ts | 1 + 20 files changed, 1388 insertions(+), 46 deletions(-) create mode 100644 apps/app/lib/hooks/useOutsideClickDetector.tsx create mode 100644 apps/app/ui/emoji-icon-picker/emojis.json create mode 100644 apps/app/ui/emoji-icon-picker/helpers.ts create mode 100644 apps/app/ui/emoji-icon-picker/index.tsx create mode 100644 apps/app/ui/emoji-icon-picker/types.d.ts diff --git a/apps/app/components/project/create-project-modal.tsx b/apps/app/components/project/create-project-modal.tsx index 5d4d9edb8..3e1d0c5a2 100644 --- a/apps/app/components/project/create-project-modal.tsx +++ b/apps/app/components/project/create-project-modal.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react"; // swr import useSWR, { mutate } from "swr"; // react hook form -import { useForm } from "react-hook-form"; +import { useForm, Controller } from "react-hook-form"; // headless import { Dialog, Transition } from "@headlessui/react"; // services @@ -18,7 +18,7 @@ import { PROJECTS_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys"; import useUser from "lib/hooks/useUser"; import useToast from "lib/hooks/useToast"; // ui -import { Button, Input, TextArea, Select } from "ui"; +import { Button, Input, TextArea, Select, EmojiIconPicker } from "ui"; // types import { IProject } from "types"; @@ -32,6 +32,7 @@ const defaultValues: Partial = { identifier: "", description: "", network: 0, + icon: "", }; const IsGuestCondition: React.FC<{ @@ -83,6 +84,7 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { reset, setError, clearErrors, + control, watch, setValue, } = useForm({ @@ -201,6 +203,22 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => {

+
+ + ( + + )} + /> +
; isSubmitting: boolean; + control: Control; }; const NETWORK_CHOICES = { "0": "Secret", "2": "Public" }; -const GeneralSettings: React.FC = ({ register, errors, setError, isSubmitting }) => { +const GeneralSettings: React.FC = ({ + register, + errors, + setError, + isSubmitting, + control, +}) => { const { activeWorkspace } = useUser(); const checkIdentifier = (slug: string, value: string) => { @@ -44,8 +52,26 @@ const GeneralSettings: React.FC = ({ register, errors, setError, isSubmit This information will be displayed to every member of the project.

-
-
+
+
+
+ + ( + + )} + /> +
+
+
= ({ register, errors, setError, isSubmit }} />
-
+
= ({ navigation, sidebarCollapse }) => { sidebarCollapse ? "justify-center" : "" }`} > - - {project?.name.charAt(0)} - + {project.icon ? ( + + {String.fromCodePoint(parseInt(project.icon))} + + ) : ( + + {project?.name.charAt(0)} + + )} + {!sidebarCollapse && ( {project?.name} diff --git a/apps/app/components/socialbuttons/google-login.tsx b/apps/app/components/socialbuttons/google-login.tsx index 62d9402c7..6c39c58af 100644 --- a/apps/app/components/socialbuttons/google-login.tsx +++ b/apps/app/components/socialbuttons/google-login.tsx @@ -1,4 +1,4 @@ -import { FC, CSSProperties, useEffect, useRef, useCallback } from "react"; +import { FC, CSSProperties, useEffect, useRef, useCallback, useState } from "react"; // next import Script from "next/script"; @@ -11,9 +11,10 @@ export interface IGoogleLoginButton { export const GoogleLoginButton: FC = (props) => { const googleSignInButton = useRef(null); + const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false); const loadScript = useCallback(() => { - if (!googleSignInButton.current) return; + if (!googleSignInButton.current || gsiScriptLoaded) return; window?.google?.accounts.id.initialize({ client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "", callback: props.onSuccess as any, @@ -30,12 +31,16 @@ export const GoogleLoginButton: FC = (props) => { } as GsiButtonConfiguration // customization attributes ); window?.google?.accounts.id.prompt(); // also display the One Tap dialog - }, [props.onSuccess]); + setGsiScriptLoaded(true); + }, [props.onSuccess, gsiScriptLoaded]); useEffect(() => { if (window?.google?.accounts?.id) { loadScript(); } + return () => { + window?.google?.accounts.id.cancel(); + }; }, [loadScript]); return ( diff --git a/apps/app/constants/api-routes.ts b/apps/app/constants/api-routes.ts index 9e282562a..24de30251 100644 --- a/apps/app/constants/api-routes.ts +++ b/apps/app/constants/api-routes.ts @@ -64,6 +64,8 @@ export const PROJECT_MEMBERS = (workspaceSlug: string, projectId: string) => `/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`; export const PROJECT_MEMBER_DETAIL = (workspaceSlug: string, projectId: string, memberId: string) => `/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`; +export const PROJECT_MEMBER_ME = (workspaceSlug: string, projectId: string) => + `/api/workspaces/${workspaceSlug}/projects/${projectId}/project-members/me/`; export const PROJECT_VIEW_ENDPOINT = (workspaceSlug: string, projectId: string) => `/api/workspaces/${workspaceSlug}/projects/${projectId}/project-views/`; diff --git a/apps/app/contexts/theme.context.tsx b/apps/app/contexts/theme.context.tsx index ee0b88e83..b363524b8 100644 --- a/apps/app/contexts/theme.context.tsx +++ b/apps/app/contexts/theme.context.tsx @@ -68,13 +68,9 @@ export const reducer: ReducerFunctionType = (state, action) => { ...state, collapsed: !state.collapsed, }; - localStorage.setItem("theme", JSON.stringify(newState)); return newState; case REHYDRATE_THEME: { - let newState: any = localStorage.getItem("theme"); - if (newState !== null) { - newState = JSON.parse(newState); - } + const newState = payload; return { ...initialState, ...newState }; } case SET_ISSUE_VIEW: { @@ -82,7 +78,6 @@ export const reducer: ReducerFunctionType = (state, action) => { ...state, issueView: payload?.issueView || "list", }; - localStorage.setItem("theme", JSON.stringify(newState)); return { ...state, ...newState, @@ -93,7 +88,6 @@ export const reducer: ReducerFunctionType = (state, action) => { ...state, groupByProperty: payload?.groupByProperty || null, }; - localStorage.setItem("theme", JSON.stringify(newState)); return { ...state, ...newState, @@ -104,7 +98,6 @@ export const reducer: ReducerFunctionType = (state, action) => { ...state, orderBy: payload?.orderBy || null, }; - localStorage.setItem("theme", JSON.stringify(newState)); return { ...state, ...newState, @@ -115,7 +108,6 @@ export const reducer: ReducerFunctionType = (state, action) => { ...state, filterIssue: payload?.filterIssue || null, }; - localStorage.setItem("theme", JSON.stringify(newState)); return { ...state, ...newState, @@ -127,6 +119,10 @@ export const reducer: ReducerFunctionType = (state, action) => { } }; +const saveDataToServer = async (workspaceSlug: string, projectID: string, state: any) => { + await projectService.setProjectView(workspaceSlug, projectID, state); +}; + export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [state, dispatch] = useReducer(reducer, initialState); @@ -145,16 +141,6 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({ }); }, []); - const saveDataToServer = useCallback(() => { - if (!activeProject || !activeWorkspace) return; - projectService - .setProjectView(activeWorkspace.slug, activeProject.id, state) - .then((res) => { - console.log("saved", res); - }) - .catch((error) => {}); - }, [activeProject, activeWorkspace, state]); - const setIssueView = useCallback( (display: "list" | "kanban") => { dispatch({ @@ -163,9 +149,14 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({ issueView: display, }, }); - saveDataToServer(); + + if (!activeWorkspace || !activeProject) return; + saveDataToServer(activeWorkspace.slug, activeProject.id, { + ...state, + issueView: display, + }); }, - [saveDataToServer] + [activeProject, activeWorkspace, state] ); const setGroupByProperty = useCallback( @@ -176,9 +167,14 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({ groupByProperty: property, }, }); - saveDataToServer(); + + if (!activeWorkspace || !activeProject) return; + saveDataToServer(activeWorkspace.slug, activeProject.id, { + ...state, + groupByProperty: property, + }); }, - [saveDataToServer] + [activeProject, activeWorkspace, state] ); const setOrderBy = useCallback( @@ -189,11 +185,12 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({ orderBy: property, }, }); - saveDataToServer(); - }, - [saveDataToServer] - ); + if (!activeWorkspace || !activeProject) return; + saveDataToServer(activeWorkspace.slug, activeProject.id, state); + }, + [activeProject, activeWorkspace, state] + ); const setFilterIssue = useCallback( (property: "activeIssue" | "backlogIssue" | null) => { dispatch({ @@ -202,9 +199,14 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({ filterIssue: property, }, }); - saveDataToServer(); + + if (!activeWorkspace || !activeProject) return; + saveDataToServer(activeWorkspace.slug, activeProject.id, { + ...state, + filterIssue: property, + }); }, - [saveDataToServer] + [activeProject, activeWorkspace, state] ); useEffect(() => { diff --git a/apps/app/lib/hooks/useIssuesFilter.tsx b/apps/app/lib/hooks/useIssuesFilter.tsx index ca75e54b7..30bc42db9 100644 --- a/apps/app/lib/hooks/useIssuesFilter.tsx +++ b/apps/app/lib/hooks/useIssuesFilter.tsx @@ -1,4 +1,3 @@ -// hooks import useTheme from "./useTheme"; import useUser from "./useUser"; // commons diff --git a/apps/app/lib/hooks/useIssuesProperties.tsx b/apps/app/lib/hooks/useIssuesProperties.tsx index ccd94e6b6..223eafe2a 100644 --- a/apps/app/lib/hooks/useIssuesProperties.tsx +++ b/apps/app/lib/hooks/useIssuesProperties.tsx @@ -18,6 +18,7 @@ const initialValues: Properties = { start_date: false, target_date: false, cycle: false, + children_count: false, }; const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => { diff --git a/apps/app/lib/hooks/useMyIssueFilter.tsx b/apps/app/lib/hooks/useMyIssueFilter.tsx index 2d16f1442..da8055a5f 100644 --- a/apps/app/lib/hooks/useMyIssueFilter.tsx +++ b/apps/app/lib/hooks/useMyIssueFilter.tsx @@ -18,6 +18,7 @@ const initialValues: Properties = { start_date: false, target_date: false, cycle: false, + children_count: false, }; const useMyIssuesProperties = (issues?: IIssue[]) => { diff --git a/apps/app/lib/hooks/useOutsideClickDetector.tsx b/apps/app/lib/hooks/useOutsideClickDetector.tsx new file mode 100644 index 000000000..f20666f8c --- /dev/null +++ b/apps/app/lib/hooks/useOutsideClickDetector.tsx @@ -0,0 +1,19 @@ +import React, { useEffect } from "react"; + +const useOutsideClickDetector = (ref: React.RefObject, callback: () => void) => { + const handleClick = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + callback(); + } + }; + + useEffect(() => { + document.addEventListener("click", handleClick); + + return () => { + document.removeEventListener("click", handleClick); + }; + }); +}; + +export default useOutsideClickDetector; diff --git a/apps/app/lib/services/project.service.ts b/apps/app/lib/services/project.service.ts index 98891c796..92996f0e5 100644 --- a/apps/app/lib/services/project.service.ts +++ b/apps/app/lib/services/project.service.ts @@ -11,6 +11,7 @@ import { PROJECT_MEMBER_DETAIL, USER_PROJECT_INVITATIONS, PROJECT_VIEW_ENDPOINT, + PROJECT_MEMBER_ME, } from "constants/api-routes"; // services import APIService from "lib/services/api.service"; @@ -132,6 +133,16 @@ class ProjectServices extends APIService { }); } + async projectMemberMe(workspacSlug: string, projectId: string): Promise { + return this.get(PROJECT_MEMBER_ME(workspacSlug, projectId)) + .then((response) => { + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + async getProjectMember( workspacSlug: string, projectId: string, diff --git a/apps/app/pages/projects/[projectId]/issues/index.tsx b/apps/app/pages/projects/[projectId]/issues/index.tsx index 9606fffcf..f0576eedc 100644 --- a/apps/app/pages/projects/[projectId]/issues/index.tsx +++ b/apps/app/pages/projects/[projectId]/issues/index.tsx @@ -142,7 +142,7 @@ const ProjectIssues: NextPage = () => { setFilterIssue, orderBy, filterIssue, - } = useIssuesFilter(projectIssues?.results ?? []); + } = useIssuesFilter(projectIssues?.results.filter((p) => p.parent === null) ?? []); useEffect(() => { if (!isOpen) { diff --git a/apps/app/pages/projects/[projectId]/settings.tsx b/apps/app/pages/projects/[projectId]/settings.tsx index 18d5ed567..5878082d1 100644 --- a/apps/app/pages/projects/[projectId]/settings.tsx +++ b/apps/app/pages/projects/[projectId]/settings.tsx @@ -100,6 +100,7 @@ const ProjectSettings: NextPage = () => { description: formData.description, default_assignee: formData.default_assignee, project_lead: formData.project_lead, + icon: formData.icon, }; await projectServices .updateProject(activeWorkspace.slug, projectId as string, payload) @@ -186,6 +187,7 @@ const ProjectSettings: NextPage = () => {
{ + const recentEmojis = localStorage.getItem("recentEmojis"); + if (recentEmojis) { + const recentEmojisArray = recentEmojis.split(","); + if (recentEmojisArray.includes(emoji)) { + const index = recentEmojisArray.indexOf(emoji); + recentEmojisArray.splice(index, 1); + } + recentEmojisArray.unshift(emoji); + if (recentEmojisArray.length > 18) { + recentEmojisArray.pop(); + } + localStorage.setItem("recentEmojis", recentEmojisArray.join(",")); + } else { + localStorage.setItem("recentEmojis", emoji); + } +}; + +export const getRecentEmojis = () => { + const recentEmojis = localStorage.getItem("recentEmojis"); + if (recentEmojis) { + const recentEmojisArray = recentEmojis.split(","); + return recentEmojisArray; + } + return []; +}; diff --git a/apps/app/ui/emoji-icon-picker/index.tsx b/apps/app/ui/emoji-icon-picker/index.tsx new file mode 100644 index 000000000..b415724c1 --- /dev/null +++ b/apps/app/ui/emoji-icon-picker/index.tsx @@ -0,0 +1,125 @@ +import React, { useEffect, useState, useRef } from "react"; +// headless ui +import { Tab, Transition, Popover } from "@headlessui/react"; +// hooks +import useOutsideClickDetector from "lib/hooks/useOutsideClickDetector"; +// emoji +import emojis from "./emojis.json"; +// helpers +import { getRecentEmojis, saveRecentEmoji } from "./helpers"; +// types +import { Props } from "./types"; + +const tabOptions = [ + { + key: "emoji", + title: "Emoji", + }, + { + key: "icon", + title: "Icon", + }, +]; + +const EmojiIconPicker: React.FC = ({ label, value, onChange }) => { + const ref = useRef(null); + + const [isOpen, setIsOpen] = useState(false); + + const [recentEmojis, setRecentEmojis] = useState([]); + + useEffect(() => { + setRecentEmojis(getRecentEmojis()); + }, []); + + useOutsideClickDetector(ref, () => { + setIsOpen(false); + }); + + return ( + + setIsOpen((prev) => !prev)} + > + {label} + + + +
+ + + {tabOptions.map((tab) => ( + + `w-1/2 py-2 transition-colors border-b text-sm font-medium text-center outline-none -my-1 ${ + selected ? "border-theme" : "border-transparent" + }` + } + > + {tab.title} + + ))} + + + + {recentEmojis.length > 0 && ( +
+

Recent Emojis

+
+ {recentEmojis.map((emoji) => ( + + ))} +
+
+ )} +
+

All Emojis

+
+ {emojis.map((emoji) => ( + + ))} +
+
+
+ +

Coming Soon...

+
+
+
+
+
+
+
+ ); +}; + +export default EmojiIconPicker; diff --git a/apps/app/ui/emoji-icon-picker/types.d.ts b/apps/app/ui/emoji-icon-picker/types.d.ts new file mode 100644 index 000000000..2d3e26353 --- /dev/null +++ b/apps/app/ui/emoji-icon-picker/types.d.ts @@ -0,0 +1,5 @@ +export type Props = { + label: string | React.ReactNode; + value: any; + onChange: (data: any) => void; +}; diff --git a/apps/app/ui/index.ts b/apps/app/ui/index.ts index 6b564eba1..94e451654 100644 --- a/apps/app/ui/index.ts +++ b/apps/app/ui/index.ts @@ -10,3 +10,4 @@ export { default as SearchListbox } from "./search-listbox"; export { default as HeaderButton } from "./HeaderButton"; export * from "./Breadcrumbs"; export * from "./EmptySpace"; +export { default as EmojiIconPicker } from "./emoji-icon-picker";