forked from github/plane
feat: made emoji-icon-picker
fix: google prompt coming up after leaving sign in, refractor: saving views data to db instead of local-storage
This commit is contained in:
parent
f52724fd86
commit
13985df860
@ -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<IProject> = {
|
||||
identifier: "",
|
||||
description: "",
|
||||
network: 0,
|
||||
icon: "",
|
||||
};
|
||||
|
||||
const IsGuestCondition: React.FC<{
|
||||
@ -83,6 +84,7 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
reset,
|
||||
setError,
|
||||
clearErrors,
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
} = useForm<IProject>({
|
||||
@ -201,6 +203,22 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label htmlFor="icon" className="text-gray-500 mb-2">
|
||||
Icon
|
||||
</label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="icon"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<EmojiIconPicker
|
||||
label={value ? String.fromCodePoint(parseInt(value)) : "Select Icon"}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
id="name"
|
||||
|
@ -1,13 +1,14 @@
|
||||
// react
|
||||
import { useCallback } from "react";
|
||||
// react-hook-form
|
||||
import { UseFormRegister, UseFormSetError } from "react-hook-form";
|
||||
import { Controller } from "react-hook-form";
|
||||
import type { Control, UseFormRegister, UseFormSetError } from "react-hook-form";
|
||||
// services
|
||||
import projectServices from "lib/services/project.service";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// ui
|
||||
import { Button, Input, Select, TextArea } from "ui";
|
||||
import { Button, Input, Select, TextArea, EmojiIconPicker } from "ui";
|
||||
// types
|
||||
import { IProject } from "types";
|
||||
// constants
|
||||
@ -18,11 +19,18 @@ type Props = {
|
||||
errors: any;
|
||||
setError: UseFormSetError<IProject>;
|
||||
isSubmitting: boolean;
|
||||
control: Control<IProject, any>;
|
||||
};
|
||||
|
||||
const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
||||
|
||||
const GeneralSettings: React.FC<Props> = ({ register, errors, setError, isSubmitting }) => {
|
||||
const GeneralSettings: React.FC<Props> = ({
|
||||
register,
|
||||
errors,
|
||||
setError,
|
||||
isSubmitting,
|
||||
control,
|
||||
}) => {
|
||||
const { activeWorkspace } = useUser();
|
||||
|
||||
const checkIdentifier = (slug: string, value: string) => {
|
||||
@ -44,8 +52,26 @@ const GeneralSettings: React.FC<Props> = ({ register, errors, setError, isSubmit
|
||||
This information will be displayed to every member of the project.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div className="col-span-2">
|
||||
<div className="grid grid-cols-10 gap-3 items-center">
|
||||
<div className="col-span-1">
|
||||
<div>
|
||||
<label htmlFor="icon" className="text-gray-500 mb-2">
|
||||
Icon
|
||||
</label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="icon"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<EmojiIconPicker
|
||||
label={value ? String.fromCodePoint(parseInt(value)) : "Select Icon"}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-5">
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
@ -58,7 +84,7 @@ const GeneralSettings: React.FC<Props> = ({ register, errors, setError, isSubmit
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="col-span-2">
|
||||
<Select
|
||||
name="network"
|
||||
id="network"
|
||||
@ -73,7 +99,7 @@ const GeneralSettings: React.FC<Props> = ({ register, errors, setError, isSubmit
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="col-span-2">
|
||||
<Input
|
||||
id="identifier"
|
||||
name="identifier"
|
||||
|
@ -62,9 +62,16 @@ const ProjectsList: React.FC<Props> = ({ navigation, sidebarCollapse }) => {
|
||||
sidebarCollapse ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
<span className="bg-gray-700 text-white rounded h-7 w-7 grid place-items-center uppercase flex-shrink-0">
|
||||
{project?.name.charAt(0)}
|
||||
</span>
|
||||
{project.icon ? (
|
||||
<span className="text-white rounded h-7 w-7 grid place-items-center uppercase flex-shrink-0">
|
||||
{String.fromCodePoint(parseInt(project.icon))}
|
||||
</span>
|
||||
) : (
|
||||
<span className="bg-gray-700 text-white rounded h-7 w-7 grid place-items-center uppercase flex-shrink-0">
|
||||
{project?.name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!sidebarCollapse && (
|
||||
<span className="flex items-center justify-between w-full">
|
||||
{project?.name}
|
||||
|
@ -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<IGoogleLoginButton> = (props) => {
|
||||
const googleSignInButton = useRef<HTMLDivElement>(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<IGoogleLoginButton> = (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 (
|
||||
|
@ -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/`;
|
||||
|
||||
|
@ -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(() => {
|
||||
|
@ -1,4 +1,3 @@
|
||||
// hooks
|
||||
import useTheme from "./useTheme";
|
||||
import useUser from "./useUser";
|
||||
// commons
|
||||
|
@ -18,6 +18,7 @@ const initialValues: Properties = {
|
||||
start_date: false,
|
||||
target_date: false,
|
||||
cycle: false,
|
||||
children_count: false,
|
||||
};
|
||||
|
||||
const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
|
||||
|
@ -18,6 +18,7 @@ const initialValues: Properties = {
|
||||
start_date: false,
|
||||
target_date: false,
|
||||
cycle: false,
|
||||
children_count: false,
|
||||
};
|
||||
|
||||
const useMyIssuesProperties = (issues?: IIssue[]) => {
|
||||
|
19
apps/app/lib/hooks/useOutsideClickDetector.tsx
Normal file
19
apps/app/lib/hooks/useOutsideClickDetector.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
const useOutsideClickDetector = (ref: React.RefObject<HTMLElement>, 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;
|
@ -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<IProjectMember> {
|
||||
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,
|
||||
|
@ -142,7 +142,7 @@ const ProjectIssues: NextPage = () => {
|
||||
setFilterIssue,
|
||||
orderBy,
|
||||
filterIssue,
|
||||
} = useIssuesFilter(projectIssues?.results ?? []);
|
||||
} = useIssuesFilter(projectIssues?.results.filter((p) => p.parent === null) ?? []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
|
@ -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 = () => {
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Tab.Panel>
|
||||
<GeneralSettings
|
||||
control={control}
|
||||
register={register}
|
||||
errors={errors}
|
||||
setError={setError}
|
||||
|
1
apps/app/types/issues.d.ts
vendored
1
apps/app/types/issues.d.ts
vendored
@ -133,6 +133,7 @@ export type Properties = {
|
||||
start_date: boolean;
|
||||
target_date: boolean;
|
||||
cycle: boolean;
|
||||
children_count: boolean;
|
||||
};
|
||||
|
||||
export interface IIssueLabels {
|
||||
|
1
apps/app/types/projects.d.ts
vendored
1
apps/app/types/projects.d.ts
vendored
@ -14,6 +14,7 @@ export interface IProject {
|
||||
slug: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
type ProjectViewTheme = {
|
||||
|
1090
apps/app/ui/emoji-icon-picker/emojis.json
Normal file
1090
apps/app/ui/emoji-icon-picker/emojis.json
Normal file
File diff suppressed because it is too large
Load Diff
26
apps/app/ui/emoji-icon-picker/helpers.ts
Normal file
26
apps/app/ui/emoji-icon-picker/helpers.ts
Normal file
@ -0,0 +1,26 @@
|
||||
export const saveRecentEmoji = (emoji: string) => {
|
||||
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 [];
|
||||
};
|
125
apps/app/ui/emoji-icon-picker/index.tsx
Normal file
125
apps/app/ui/emoji-icon-picker/index.tsx
Normal file
@ -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<Props> = ({ label, value, onChange }) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setRecentEmojis(getRecentEmojis());
|
||||
}, []);
|
||||
|
||||
useOutsideClickDetector(ref, () => {
|
||||
setIsOpen(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<Popover className="relative" ref={ref}>
|
||||
<Popover.Button
|
||||
className="bg-gray-100 px-3 py-1 rounded-full outline-none"
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
>
|
||||
{label}
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
show={isOpen}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Popover.Panel className="absolute z-10 w-80 bg-white rounded-md shadow-lg mt-2">
|
||||
<div className="w-80 h-80 p-2 bg-white overflow-auto rounded shadow-2xl border">
|
||||
<Tab.Group as="div" className="w-full h-full flex flex-col">
|
||||
<Tab.List className="-mx-2 flex justify-around p-1 gap-1 border-b rounded flex-0">
|
||||
{tabOptions.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className={({ selected }) =>
|
||||
`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}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels className="w-full h-full flex-1 overflow-y-auto overflow-x-hidden">
|
||||
<Tab.Panel className="w-full h-full">
|
||||
{recentEmojis.length > 0 && (
|
||||
<div className="py-2 w-full">
|
||||
<h3 className="text-lg mb-2">Recent Emojis</h3>
|
||||
<div className="grid grid-cols-9 gap-2">
|
||||
{recentEmojis.map((emoji) => (
|
||||
<button
|
||||
type="button"
|
||||
className="select-none text-xl"
|
||||
key={emoji}
|
||||
onClick={() => {
|
||||
onChange(emoji);
|
||||
}}
|
||||
>
|
||||
{String.fromCodePoint(parseInt(emoji))}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="py-3">
|
||||
<h3 className="text-lg mb-2">All Emojis</h3>
|
||||
<div className="grid grid-cols-9 gap-2">
|
||||
{emojis.map((emoji) => (
|
||||
<button
|
||||
type="button"
|
||||
className="select-none text-xl"
|
||||
key={emoji}
|
||||
onClick={() => {
|
||||
onChange(emoji);
|
||||
saveRecentEmoji(emoji);
|
||||
}}
|
||||
>
|
||||
{String.fromCodePoint(parseInt(emoji))}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel className="w-full h-full flex flex-col justify-center items-center">
|
||||
<p>Coming Soon...</p>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmojiIconPicker;
|
5
apps/app/ui/emoji-icon-picker/types.d.ts
vendored
Normal file
5
apps/app/ui/emoji-icon-picker/types.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
export type Props = {
|
||||
label: string | React.ReactNode;
|
||||
value: any;
|
||||
onChange: (data: any) => void;
|
||||
};
|
@ -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";
|
||||
|
Loading…
Reference in New Issue
Block a user