@greatapps/common 1.1.378 → 1.1.380
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/navigation/ProjectSelectorPopover.mjs +69 -40
- package/dist/components/navigation/ProjectSelectorPopover.mjs.map +1 -1
- package/dist/components/navigation/subcomponents/ProjectSelector.mjs +59 -32
- package/dist/components/navigation/subcomponents/ProjectSelector.mjs.map +1 -1
- package/dist/infra/api/types.mjs +1 -1
- package/dist/infra/api/types.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/navigation/ProjectSelectorPopover.tsx +42 -8
- package/src/components/navigation/subcomponents/ProjectSelector.tsx +36 -6
- package/src/infra/api/types.ts +4 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback } from "react";
|
|
3
4
|
import { ADDON_IDS } from "../../modules/subscriptions/constants/addons.constants";
|
|
4
5
|
import { useHasPlanAddon } from "../../modules/plans/hooks/use-has-plan-addon.hook";
|
|
5
6
|
import {
|
|
@@ -16,10 +17,10 @@ import {
|
|
|
16
17
|
} from "../../index";
|
|
17
18
|
import { IconCheck, IconChevronDown, IconFolderOpen, IconPlus } from "@tabler/icons-react";
|
|
18
19
|
function ProjectItemSkeleton() {
|
|
19
|
-
return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-2 py-
|
|
20
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-2 py-2 gap-2", children: [
|
|
20
21
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-1", children: [
|
|
21
|
-
/* @__PURE__ */ jsx("div", { className: "skeleton size-
|
|
22
|
-
/* @__PURE__ */ jsx("div", { className: "skeleton h-
|
|
22
|
+
/* @__PURE__ */ jsx("div", { className: "skeleton size-9 rounded-lg shrink-0" }),
|
|
23
|
+
/* @__PURE__ */ jsx("div", { className: "skeleton h-4 w-36 rounded" })
|
|
23
24
|
] }),
|
|
24
25
|
/* @__PURE__ */ jsx("div", { className: "skeleton size-4 rounded-full shrink-0" })
|
|
25
26
|
] });
|
|
@@ -30,12 +31,24 @@ function ProjectSelectorPopover({
|
|
|
30
31
|
titleRef,
|
|
31
32
|
projects,
|
|
32
33
|
selectedProject,
|
|
34
|
+
isLoading,
|
|
35
|
+
isFetchingNextPage,
|
|
36
|
+
hasNextPage,
|
|
37
|
+
onLoadMore,
|
|
33
38
|
onSelectProject,
|
|
34
39
|
onAddProject,
|
|
35
40
|
onManageProjects,
|
|
36
41
|
children
|
|
37
42
|
}) {
|
|
38
43
|
const { hasAddon: canAddProject } = useHasPlanAddon(ADDON_IDS.PROJECTS);
|
|
44
|
+
const handleListScroll = useCallback((event) => {
|
|
45
|
+
if (!hasNextPage || isFetchingNextPage || !onLoadMore) return;
|
|
46
|
+
const target = event.currentTarget;
|
|
47
|
+
const distanceToBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
|
|
48
|
+
if (distanceToBottom <= 80) {
|
|
49
|
+
onLoadMore();
|
|
50
|
+
}
|
|
51
|
+
}, [hasNextPage, isFetchingNextPage, onLoadMore]);
|
|
39
52
|
return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
|
|
40
53
|
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: children ?? /* @__PURE__ */ jsx("button", { className: "cursor-pointer", children: /* @__PURE__ */ jsx(
|
|
41
54
|
IconChevronDown,
|
|
@@ -74,43 +87,59 @@ function ProjectSelectorPopover({
|
|
|
74
87
|
] }),
|
|
75
88
|
/* @__PURE__ */ jsxs(Command, { className: "gap-2", children: [
|
|
76
89
|
/* @__PURE__ */ jsx(CommandInput, { placeholder: "Busque aqui" }),
|
|
77
|
-
/* @__PURE__ */ jsx(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
),
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
90
|
+
/* @__PURE__ */ jsx(
|
|
91
|
+
CommandList,
|
|
92
|
+
{
|
|
93
|
+
className: "custom-scrollbar paragraph-small-medium text-gray-600 h-[227px]",
|
|
94
|
+
onScroll: handleListScroll,
|
|
95
|
+
children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
96
|
+
/* @__PURE__ */ jsx(ProjectItemSkeleton, {}),
|
|
97
|
+
/* @__PURE__ */ jsx(ProjectItemSkeleton, {}),
|
|
98
|
+
/* @__PURE__ */ jsx(ProjectItemSkeleton, {}),
|
|
99
|
+
/* @__PURE__ */ jsx(ProjectItemSkeleton, {})
|
|
100
|
+
] }) : projects.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center gap-2 h-full text-gray-400 py-4", children: [
|
|
101
|
+
/* @__PURE__ */ jsx(IconFolderOpen, { size: 28, className: "text-gray-300" }),
|
|
102
|
+
/* @__PURE__ */ jsxs("span", { className: "paragraph-small-medium text-center text-gray-500", children: [
|
|
103
|
+
"Nenhum projeto encontrado.",
|
|
104
|
+
/* @__PURE__ */ jsx("br", {}),
|
|
105
|
+
"Crie um projeto para come\xE7ar."
|
|
106
|
+
] })
|
|
107
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
108
|
+
/* @__PURE__ */ jsx(CommandEmpty, { children: "Nenhum resultado encontrado." }),
|
|
109
|
+
projects.map((project) => {
|
|
110
|
+
const isSelected = selectedProject?.id === project.id;
|
|
111
|
+
return /* @__PURE__ */ jsx(
|
|
112
|
+
CommandItem,
|
|
113
|
+
{
|
|
114
|
+
value: `${project.title}-${project.id}`,
|
|
115
|
+
keywords: [project.title],
|
|
116
|
+
className: isSelected ? "bg-gray-50 min-h-[39px]" : "min-h-[39px]",
|
|
117
|
+
onSelect: () => onSelectProject(project),
|
|
118
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between w-full cursor-pointer", children: [
|
|
119
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 w-full", children: [
|
|
120
|
+
/* @__PURE__ */ jsx(
|
|
121
|
+
UserAvatar,
|
|
122
|
+
{
|
|
123
|
+
photo: project.photo,
|
|
124
|
+
name: project.title,
|
|
125
|
+
size: 36
|
|
126
|
+
}
|
|
127
|
+
),
|
|
128
|
+
project.title
|
|
129
|
+
] }),
|
|
130
|
+
isSelected && /* @__PURE__ */ jsx(IconCheck, { className: "size-4 text-gray-950" })
|
|
131
|
+
] })
|
|
132
|
+
},
|
|
133
|
+
project.id
|
|
134
|
+
);
|
|
135
|
+
}),
|
|
136
|
+
isFetchingNextPage && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
137
|
+
/* @__PURE__ */ jsx(ProjectItemSkeleton, {}),
|
|
138
|
+
/* @__PURE__ */ jsx(ProjectItemSkeleton, {})
|
|
139
|
+
] })
|
|
140
|
+
] })
|
|
141
|
+
}
|
|
142
|
+
)
|
|
114
143
|
] }),
|
|
115
144
|
onManageProjects && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
116
145
|
/* @__PURE__ */ jsx("div", { className: "border-t border-gray-200" }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/navigation/ProjectSelectorPopover.tsx"],"sourcesContent":["'use client';\n\nimport { Dispatch, RefObject, SetStateAction } from 'react';\nimport { ADDON_IDS } from '../../modules/subscriptions/constants/addons.constants';\nimport { useHasPlanAddon } from '../../modules/plans/hooks/use-has-plan-addon.hook';\nimport {\n Button,\n Command,\n CommandEmpty,\n CommandInput,\n CommandItem,\n CommandList,\n Popover,\n PopoverContent,\n PopoverTrigger,\n UserAvatar,\n} from '../../index';\nimport { IconCheck, IconChevronDown, IconFolderOpen, IconPlus } from '@tabler/icons-react';\nimport type { NavigationProject } from './types';\n\ninterface ProjectSelectorPopoverProps {\n open: boolean;\n setOpen: Dispatch<SetStateAction<boolean>>;\n titleRef?: RefObject<HTMLSpanElement | null>;\n projects: NavigationProject[];\n selectedProject: NavigationProject | null;\n onSelectProject: (project: NavigationProject) => void;\n onAddProject?: () => void;\n onManageProjects?: () => void;\n children?: React.ReactNode;\n}\n\nfunction ProjectItemSkeleton() {\n return (\n <div className=\"flex items-center justify-between px-2 py-1.5 gap-2\">\n <div className=\"flex items-center gap-2 flex-1\">\n <div className=\"skeleton size-8 rounded-lg shrink-0\" />\n <div className=\"skeleton h-3.5 w-32 rounded\" />\n </div>\n <div className=\"skeleton size-4 rounded-full shrink-0\" />\n </div>\n );\n}\n\nexport function ProjectSelectorPopover({\n open,\n setOpen,\n titleRef,\n projects,\n selectedProject,\n onSelectProject,\n onAddProject,\n onManageProjects,\n children,\n}: ProjectSelectorPopoverProps) {\n const { hasAddon: canAddProject } = useHasPlanAddon(ADDON_IDS.PROJECTS);\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n {children ?? (\n <button className=\"cursor-pointer\">\n <IconChevronDown\n size={20}\n className={`transition-transform duration-200 ${open ? 'rotate-180 text-gray-950' : 'text-gray-400'}`}\n />\n </button>\n )}\n </PopoverTrigger>\n <PopoverContent\n className=\"w-[296px] p-0 bg-white rounded-2xl shadow-md border border-gray-200\"\n align=\"start\"\n sideOffset={8}\n onOpenAutoFocus={(e: Event) => e.preventDefault()}\n onInteractOutside={(event: Event) => {\n if (titleRef?.current?.contains(event.target as Node)) {\n event.preventDefault();\n }\n }}\n >\n <div className=\"flex flex-col p-3 gap-2\">\n <div className=\"flex items-center justify-between\">\n <span className=\"paragraph-small-semibold\">Projetos</span>\n {(onAddProject || onManageProjects) && (\n <div className=\"flex items-center gap-1.5\">\n {canAddProject && onAddProject && (\n <Button\n variant=\"secondary\"\n className=\"h-8! paragraph-small-semibold text-gray-950\"\n onClick={onAddProject}\n >\n <IconPlus size={16} />\n Adicionar\n </Button>\n )}\n </div>\n )}\n </div>\n <Command className=\"gap-2\">\n <CommandInput placeholder=\"Busque aqui\" />\n <CommandList className=\"custom-scrollbar paragraph-small-medium text-gray-600 h-[162px]\">\n {projects.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center gap-2 h-full text-gray-400 py-4\">\n <IconFolderOpen size={28} className=\"text-gray-300\" />\n <span className=\"paragraph-small-medium text-center text-gray-500\">\n Nenhum projeto encontrado.\n <br />\n Crie um projeto para começar.\n </span>\n </div>\n ) : (\n <>\n <CommandEmpty>Nenhum resultado encontrado.</CommandEmpty>\n {projects.map((project) => {\n const isSelected = selectedProject?.id === project.id;\n return (\n <CommandItem\n key={project.id}\n value={`${project.title}-${project.id}`}\n keywords={[project.title]}\n className={isSelected ? 'bg-gray-50' : ''}\n onSelect={() => onSelectProject(project)}\n >\n <div className=\"flex items-center justify-between w-full cursor-pointer\">\n <div className=\"flex items-center gap-2 w-full\">\n <UserAvatar\n photo={project.photo}\n name={project.title}\n size={32}\n />\n {project.title}\n </div>\n {isSelected && (\n <IconCheck className=\"size-4 text-gray-950\" />\n )}\n </div>\n </CommandItem>\n );\n })}\n </>\n )}\n </CommandList>\n </Command>\n {onManageProjects && (\n <>\n <div className=\"border-t border-gray-200\" />\n <Button\n variant=\"ghost\"\n className=\"h-10! paragraph-small-medium text-gray-600 hover:text-gray-950 hover:bg-zinc-50 w-full justify-center\"\n onClick={onManageProjects}\n >\n Gerenciar projetos\n </Button>\n </>\n )}\n </div>\n </PopoverContent>\n </Popover>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/navigation/ProjectSelectorPopover.tsx"],"sourcesContent":["'use client';\n\nimport { Dispatch, RefObject, SetStateAction, UIEvent, useCallback } from 'react';\nimport { ADDON_IDS } from '../../modules/subscriptions/constants/addons.constants';\nimport { useHasPlanAddon } from '../../modules/plans/hooks/use-has-plan-addon.hook';\nimport {\n Button,\n Command,\n CommandEmpty,\n CommandInput,\n CommandItem,\n CommandList,\n Popover,\n PopoverContent,\n PopoverTrigger,\n UserAvatar,\n} from '../../index';\nimport { IconCheck, IconChevronDown, IconFolderOpen, IconPlus } from '@tabler/icons-react';\nimport type { NavigationProject } from './types';\n\ninterface ProjectSelectorPopoverProps {\n open: boolean;\n setOpen: Dispatch<SetStateAction<boolean>>;\n titleRef?: RefObject<HTMLSpanElement | null>;\n projects: NavigationProject[];\n selectedProject: NavigationProject | null;\n isLoading?: boolean;\n isFetchingNextPage?: boolean;\n hasNextPage?: boolean;\n onLoadMore?: () => void | Promise<unknown>;\n onSelectProject: (project: NavigationProject) => void;\n onAddProject?: () => void;\n onManageProjects?: () => void;\n children?: React.ReactNode;\n}\n\nfunction ProjectItemSkeleton() {\n return (\n <div className=\"flex items-center justify-between px-2 py-2 gap-2\">\n <div className=\"flex items-center gap-2 flex-1\">\n <div className=\"skeleton size-9 rounded-lg shrink-0\" />\n <div className=\"skeleton h-4 w-36 rounded\" />\n </div>\n <div className=\"skeleton size-4 rounded-full shrink-0\" />\n </div>\n );\n}\n\nexport function ProjectSelectorPopover({\n open,\n setOpen,\n titleRef,\n projects,\n selectedProject,\n isLoading,\n isFetchingNextPage,\n hasNextPage,\n onLoadMore,\n onSelectProject,\n onAddProject,\n onManageProjects,\n children,\n}: ProjectSelectorPopoverProps) {\n const { hasAddon: canAddProject } = useHasPlanAddon(ADDON_IDS.PROJECTS);\n const handleListScroll = useCallback((event: UIEvent<HTMLDivElement>) => {\n if (!hasNextPage || isFetchingNextPage || !onLoadMore) return;\n\n const target = event.currentTarget;\n const distanceToBottom = target.scrollHeight - target.scrollTop - target.clientHeight;\n\n if (distanceToBottom <= 80) {\n onLoadMore();\n }\n }, [hasNextPage, isFetchingNextPage, onLoadMore]);\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n {children ?? (\n <button className=\"cursor-pointer\">\n <IconChevronDown\n size={20}\n className={`transition-transform duration-200 ${open ? 'rotate-180 text-gray-950' : 'text-gray-400'}`}\n />\n </button>\n )}\n </PopoverTrigger>\n <PopoverContent\n className=\"w-[296px] p-0 bg-white rounded-2xl shadow-md border border-gray-200\"\n align=\"start\"\n sideOffset={8}\n onOpenAutoFocus={(e: Event) => e.preventDefault()}\n onInteractOutside={(event: Event) => {\n if (titleRef?.current?.contains(event.target as Node)) {\n event.preventDefault();\n }\n }}\n >\n <div className=\"flex flex-col p-3 gap-2\">\n <div className=\"flex items-center justify-between\">\n <span className=\"paragraph-small-semibold\">Projetos</span>\n {(onAddProject || onManageProjects) && (\n <div className=\"flex items-center gap-1.5\">\n {canAddProject && onAddProject && (\n <Button\n variant=\"secondary\"\n className=\"h-8! paragraph-small-semibold text-gray-950\"\n onClick={onAddProject}\n >\n <IconPlus size={16} />\n Adicionar\n </Button>\n )}\n </div>\n )}\n </div>\n <Command className=\"gap-2\">\n <CommandInput placeholder=\"Busque aqui\" />\n <CommandList\n className=\"custom-scrollbar paragraph-small-medium text-gray-600 h-[227px]\"\n onScroll={handleListScroll}\n >\n {isLoading ? (\n <>\n <ProjectItemSkeleton />\n <ProjectItemSkeleton />\n <ProjectItemSkeleton />\n <ProjectItemSkeleton />\n </>\n ) : projects.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center gap-2 h-full text-gray-400 py-4\">\n <IconFolderOpen size={28} className=\"text-gray-300\" />\n <span className=\"paragraph-small-medium text-center text-gray-500\">\n Nenhum projeto encontrado.\n <br />\n Crie um projeto para começar.\n </span>\n </div>\n ) : (\n <>\n <CommandEmpty>Nenhum resultado encontrado.</CommandEmpty>\n {projects.map((project) => {\n const isSelected = selectedProject?.id === project.id;\n return (\n <CommandItem\n key={project.id}\n value={`${project.title}-${project.id}`}\n keywords={[project.title]}\n className={isSelected ? 'bg-gray-50 min-h-[39px]' : 'min-h-[39px]'}\n onSelect={() => onSelectProject(project)}\n >\n <div className=\"flex items-center justify-between w-full cursor-pointer\">\n <div className=\"flex items-center gap-2 w-full\">\n <UserAvatar\n photo={project.photo}\n name={project.title}\n size={36}\n />\n {project.title}\n </div>\n {isSelected && (\n <IconCheck className=\"size-4 text-gray-950\" />\n )}\n </div>\n </CommandItem>\n );\n })}\n {isFetchingNextPage && (\n <>\n <ProjectItemSkeleton />\n <ProjectItemSkeleton />\n </>\n )}\n </>\n )}\n </CommandList>\n </Command>\n {onManageProjects && (\n <>\n <div className=\"border-t border-gray-200\" />\n <Button\n variant=\"ghost\"\n className=\"h-10! paragraph-small-medium text-gray-600 hover:text-gray-950 hover:bg-zinc-50 w-full justify-center\"\n onClick={onManageProjects}\n >\n Gerenciar projetos\n </Button>\n </>\n )}\n </div>\n </PopoverContent>\n </Popover>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -10,15 +10,18 @@ import { UsersSelectorPopover } from "../../layouts/UsersSelectorPopover";
|
|
|
10
10
|
import { Button, Command, CommandEmpty, CommandInput, CommandItem, CommandList, UserAvatar } from "../../../index";
|
|
11
11
|
import { IconChevronDown, IconPlus, IconSettings } from "@tabler/icons-react";
|
|
12
12
|
function ProjectItemSkeleton() {
|
|
13
|
-
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-
|
|
14
|
-
/* @__PURE__ */ jsx("div", { className: "skeleton size-
|
|
15
|
-
/* @__PURE__ */ jsx("div", { className: "skeleton h-
|
|
13
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-[8.8px]", children: [
|
|
14
|
+
/* @__PURE__ */ jsx("div", { className: "skeleton size-9 rounded-lg shrink-0" }),
|
|
15
|
+
/* @__PURE__ */ jsx("div", { className: "skeleton h-4 w-36 rounded" })
|
|
16
16
|
] });
|
|
17
17
|
}
|
|
18
18
|
function ProjectSelector({
|
|
19
19
|
projects,
|
|
20
20
|
selectedProject,
|
|
21
21
|
isLoading,
|
|
22
|
+
isFetchingNextPage,
|
|
23
|
+
hasNextPage,
|
|
24
|
+
onLoadMore,
|
|
22
25
|
onSelectProject,
|
|
23
26
|
onAddProject,
|
|
24
27
|
onManageProjects
|
|
@@ -45,6 +48,10 @@ function ProjectSelector({
|
|
|
45
48
|
titleRef,
|
|
46
49
|
projects,
|
|
47
50
|
selectedProject,
|
|
51
|
+
isLoading,
|
|
52
|
+
isFetchingNextPage,
|
|
53
|
+
hasNextPage,
|
|
54
|
+
onLoadMore,
|
|
48
55
|
onSelectProject,
|
|
49
56
|
onAddProject,
|
|
50
57
|
onManageProjects,
|
|
@@ -97,36 +104,56 @@ function ProjectSelector({
|
|
|
97
104
|
] }),
|
|
98
105
|
/* @__PURE__ */ jsxs(Command, { className: "gap-2", children: [
|
|
99
106
|
/* @__PURE__ */ jsx(CommandInput, { placeholder: "Busque aqui" }),
|
|
100
|
-
/* @__PURE__ */ jsxs(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
onSelect: () => {
|
|
112
|
-
onSelectProject(project);
|
|
113
|
-
setActiveSection(null);
|
|
114
|
-
},
|
|
115
|
-
children: [
|
|
116
|
-
/* @__PURE__ */ jsx(
|
|
117
|
-
UserAvatar,
|
|
118
|
-
{
|
|
119
|
-
photo: project.photo,
|
|
120
|
-
name: project.title,
|
|
121
|
-
size: 32
|
|
122
|
-
}
|
|
123
|
-
),
|
|
124
|
-
project.title
|
|
125
|
-
]
|
|
107
|
+
/* @__PURE__ */ jsxs(
|
|
108
|
+
CommandList,
|
|
109
|
+
{
|
|
110
|
+
className: "custom-scrollbar paragraph-small-medium text-gray-600 h-[227px]",
|
|
111
|
+
onScroll: (event) => {
|
|
112
|
+
if (!hasNextPage || isFetchingNextPage || !onLoadMore) return;
|
|
113
|
+
const target = event.currentTarget;
|
|
114
|
+
const distanceToBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
|
|
115
|
+
if (distanceToBottom <= 80) {
|
|
116
|
+
onLoadMore();
|
|
117
|
+
}
|
|
126
118
|
},
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
119
|
+
children: [
|
|
120
|
+
/* @__PURE__ */ jsx(CommandEmpty, { children: "Nenhum resultado encontrado." }),
|
|
121
|
+
isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
122
|
+
/* @__PURE__ */ jsx(ProjectItemSkeleton, {}),
|
|
123
|
+
/* @__PURE__ */ jsx(ProjectItemSkeleton, {}),
|
|
124
|
+
/* @__PURE__ */ jsx(ProjectItemSkeleton, {}),
|
|
125
|
+
/* @__PURE__ */ jsx(ProjectItemSkeleton, {})
|
|
126
|
+
] }) : projects.map((project) => /* @__PURE__ */ jsxs(
|
|
127
|
+
CommandItem,
|
|
128
|
+
{
|
|
129
|
+
value: `${project.title}-${project.id}`,
|
|
130
|
+
keywords: [project.title],
|
|
131
|
+
className: selectedProject?.id === project.id ? "bg-gray-50 min-h-[39px]" : "min-h-[39px]",
|
|
132
|
+
onSelect: () => {
|
|
133
|
+
onSelectProject(project);
|
|
134
|
+
setActiveSection(null);
|
|
135
|
+
},
|
|
136
|
+
children: [
|
|
137
|
+
/* @__PURE__ */ jsx(
|
|
138
|
+
UserAvatar,
|
|
139
|
+
{
|
|
140
|
+
photo: project.photo,
|
|
141
|
+
name: project.title,
|
|
142
|
+
size: 36
|
|
143
|
+
}
|
|
144
|
+
),
|
|
145
|
+
project.title
|
|
146
|
+
]
|
|
147
|
+
},
|
|
148
|
+
project.id
|
|
149
|
+
)),
|
|
150
|
+
isFetchingNextPage && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
151
|
+
/* @__PURE__ */ jsx(ProjectItemSkeleton, {}),
|
|
152
|
+
/* @__PURE__ */ jsx(ProjectItemSkeleton, {})
|
|
153
|
+
] })
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
)
|
|
130
157
|
] })
|
|
131
158
|
] })
|
|
132
159
|
] });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/navigation/subcomponents/ProjectSelector.tsx"],"sourcesContent":["'use client';\n\nimport { useState, useRef, useEffect, RefObject } from 'react';\nimport { ADDON_IDS } from '../../../modules/subscriptions/constants/addons.constants';\nimport { useHasPlanAddon } from '../../../modules/plans/hooks/use-has-plan-addon.hook';\nimport { useMobileNavbarSheet } from '../../../store/useMobileNavbarSheet';\nimport { useAppNavigationContext } from '../AppNavigationContext';\nimport { ProjectSelectorPopover } from '../ProjectSelectorPopover';\nimport { UsersSelectorPopover } from '../../layouts/UsersSelectorPopover';\nimport { Button, Command, CommandEmpty, CommandInput, CommandItem, CommandList, UserAvatar } from '../../../index';\nimport { IconChevronDown, IconPlus, IconSettings } from '@tabler/icons-react';\nimport type { NavigationProject } from '../types';\n\nexport interface ProjectSelectorProps {\n projects: NavigationProject[];\n selectedProject: NavigationProject | null;\n isLoading?: boolean;\n onSelectProject: (project: NavigationProject) => void;\n onAddProject?: () => void;\n onManageProjects?: () => void;\n}\n\nfunction ProjectItemSkeleton() {\n return (\n <div className=\"flex items-center gap-2 p-2\">\n <div className=\"skeleton size-8 rounded-lg shrink-0\" />\n <div className=\"skeleton h-3.5 w-32 rounded\" />\n </div>\n );\n}\n\nexport function ProjectSelector({\n projects,\n selectedProject,\n isLoading,\n onSelectProject,\n onAddProject,\n onManageProjects,\n}: ProjectSelectorProps) {\n const { breakpoint } = useAppNavigationContext();\n const { hasAddon: canAddProject } = useHasPlanAddon(ADDON_IDS.PROJECTS);\n const [isPopoverOpen, setIsPopoverOpen] = useState(false);\n const [activeSection, setActiveSection] = useState<'projects' | null>(null);\n const titleRef = useRef<HTMLDivElement>(null);\n const { openProjectList, clearProjectList } = useMobileNavbarSheet();\n\n useEffect(() => {\n if (openProjectList && breakpoint === 'mobile') {\n setActiveSection('projects');\n clearProjectList();\n }\n }, [openProjectList, breakpoint, clearProjectList]);\n\n // Desktop and md: title + popover + users selector (same visual as current gpages sidebars)\n if (breakpoint === 'desktop' || breakpoint === 'md') {\n return (\n <div className=\"flex items-center justify-between\">\n <ProjectSelectorPopover\n open={isPopoverOpen}\n setOpen={setIsPopoverOpen}\n titleRef={titleRef as unknown as RefObject<HTMLSpanElement | null>}\n projects={projects}\n selectedProject={selectedProject}\n onSelectProject={onSelectProject}\n onAddProject={onAddProject}\n onManageProjects={onManageProjects}\n >\n <div className=\"flex items-center gap-2 truncate select-none cursor-pointer\" ref={titleRef}>\n {isLoading ? (\n <div className=\"skeleton h-4 w-32 rounded\" />\n ) : (\n <span className=\"paragraph-medium-semibold text-gray-600 truncate max-w-[120px] inline-block\">\n {selectedProject?.title}\n </span>\n )}\n <IconChevronDown\n size={20}\n className={`transition-transform duration-200 ${isPopoverOpen ? 'rotate-180 text-gray-950' : 'text-gray-400'}`}\n />\n </div>\n </ProjectSelectorPopover>\n <div className=\"flex items-center gap-2\">\n <UsersSelectorPopover projectId={selectedProject?.id} />\n </div>\n </div>\n );\n }\n\n // Mobile: inline collapsible accordion\n return (\n <div className=\"flex flex-col gap-2.5\">\n <div className=\"flex items-center justify-between\">\n <div\n className=\"flex items-center gap-1 cursor-pointer\"\n onClick={() => setActiveSection(activeSection === 'projects' ? null : 'projects')}\n >\n <span className=\"paragraph-medium-semibold text-gray-600\">{selectedProject?.title}</span>\n <IconChevronDown\n size={20}\n className={`transition-transform duration-200 ${\n activeSection === 'projects' ? 'rotate-180 text-gray-950' : 'text-gray-400'\n }`}\n />\n </div>\n <UsersSelectorPopover projectId={selectedProject?.id} />\n </div>\n {activeSection === 'projects' && (\n <div className=\"flex flex-col p-3 gap-2 border border-gray-200 rounded-lg animate-in fade-in duration-400\">\n <div className=\"flex items-center justify-between\">\n <span className=\"paragraph-small-semibold\">Projetos</span>\n {(onAddProject || onManageProjects) && (\n <div className=\"flex items-center gap-1.5\">\n {canAddProject && onAddProject && (\n <Button variant=\"secondary\" className=\"h-8! paragraph-small-semibold text-gray-950\" onClick={onAddProject}>\n <IconPlus size={16} />\n Adicionar\n </Button>\n )}\n {onManageProjects && (\n <Button variant=\"secondary\" className=\"p-0 size-8\" onClick={onManageProjects}>\n <IconSettings size={18} />\n </Button>\n )}\n </div>\n )}\n </div>\n <Command className=\"gap-2\">\n <CommandInput placeholder=\"Busque aqui\" />\n <CommandList className=\"custom-scrollbar paragraph-small-medium text-gray-600 h-[162px]\">\n <CommandEmpty>Nenhum resultado encontrado.</CommandEmpty>\n {isLoading ? (\n <>\n <ProjectItemSkeleton />\n <ProjectItemSkeleton />\n </>\n ) : (\n projects.map((project) => (\n <CommandItem\n key={project.id}\n value={`${project.title}-${project.id}`}\n keywords={[project.title]}\n className={selectedProject?.id === project.id ? 'bg-gray-50' : ''}\n onSelect={() => { onSelectProject(project); setActiveSection(null); }}\n >\n <UserAvatar\n photo={project.photo}\n name={project.title}\n size={32}\n />\n {project.title}\n </CommandItem>\n ))\n )}\n </CommandList>\n </Command>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/navigation/subcomponents/ProjectSelector.tsx"],"sourcesContent":["'use client';\n\nimport { useState, useRef, useEffect, RefObject } from 'react';\nimport { ADDON_IDS } from '../../../modules/subscriptions/constants/addons.constants';\nimport { useHasPlanAddon } from '../../../modules/plans/hooks/use-has-plan-addon.hook';\nimport { useMobileNavbarSheet } from '../../../store/useMobileNavbarSheet';\nimport { useAppNavigationContext } from '../AppNavigationContext';\nimport { ProjectSelectorPopover } from '../ProjectSelectorPopover';\nimport { UsersSelectorPopover } from '../../layouts/UsersSelectorPopover';\nimport { Button, Command, CommandEmpty, CommandInput, CommandItem, CommandList, UserAvatar } from '../../../index';\nimport { IconChevronDown, IconPlus, IconSettings } from '@tabler/icons-react';\nimport type { NavigationProject } from '../types';\n\nexport interface ProjectSelectorProps {\n projects: NavigationProject[];\n selectedProject: NavigationProject | null;\n isLoading?: boolean;\n isFetchingNextPage?: boolean;\n hasNextPage?: boolean;\n onLoadMore?: () => void | Promise<unknown>;\n onSelectProject: (project: NavigationProject) => void;\n onAddProject?: () => void;\n onManageProjects?: () => void;\n}\n\nfunction ProjectItemSkeleton() {\n return (\n <div className=\"flex items-center gap-2 p-[8.8px]\">\n <div className=\"skeleton size-9 rounded-lg shrink-0\" />\n <div className=\"skeleton h-4 w-36 rounded\" />\n </div>\n );\n}\n\nexport function ProjectSelector({\n projects,\n selectedProject,\n isLoading,\n isFetchingNextPage,\n hasNextPage,\n onLoadMore,\n onSelectProject,\n onAddProject,\n onManageProjects,\n}: ProjectSelectorProps) {\n const { breakpoint } = useAppNavigationContext();\n const { hasAddon: canAddProject } = useHasPlanAddon(ADDON_IDS.PROJECTS);\n const [isPopoverOpen, setIsPopoverOpen] = useState(false);\n const [activeSection, setActiveSection] = useState<'projects' | null>(null);\n const titleRef = useRef<HTMLDivElement>(null);\n const { openProjectList, clearProjectList } = useMobileNavbarSheet();\n\n useEffect(() => {\n if (openProjectList && breakpoint === 'mobile') {\n setActiveSection('projects');\n clearProjectList();\n }\n }, [openProjectList, breakpoint, clearProjectList]);\n\n // Desktop and md: title + popover + users selector (same visual as current gpages sidebars)\n if (breakpoint === 'desktop' || breakpoint === 'md') {\n return (\n <div className=\"flex items-center justify-between\">\n <ProjectSelectorPopover\n open={isPopoverOpen}\n setOpen={setIsPopoverOpen}\n titleRef={titleRef as unknown as RefObject<HTMLSpanElement | null>}\n projects={projects}\n selectedProject={selectedProject}\n isLoading={isLoading}\n isFetchingNextPage={isFetchingNextPage}\n hasNextPage={hasNextPage}\n onLoadMore={onLoadMore}\n onSelectProject={onSelectProject}\n onAddProject={onAddProject}\n onManageProjects={onManageProjects}\n >\n <div className=\"flex items-center gap-2 truncate select-none cursor-pointer\" ref={titleRef}>\n {isLoading ? (\n <div className=\"skeleton h-4 w-32 rounded\" />\n ) : (\n <span className=\"paragraph-medium-semibold text-gray-600 truncate max-w-[120px] inline-block\">\n {selectedProject?.title}\n </span>\n )}\n <IconChevronDown\n size={20}\n className={`transition-transform duration-200 ${isPopoverOpen ? 'rotate-180 text-gray-950' : 'text-gray-400'}`}\n />\n </div>\n </ProjectSelectorPopover>\n <div className=\"flex items-center gap-2\">\n <UsersSelectorPopover projectId={selectedProject?.id} />\n </div>\n </div>\n );\n }\n\n // Mobile: inline collapsible accordion\n return (\n <div className=\"flex flex-col gap-2.5\">\n <div className=\"flex items-center justify-between\">\n <div\n className=\"flex items-center gap-1 cursor-pointer\"\n onClick={() => setActiveSection(activeSection === 'projects' ? null : 'projects')}\n >\n <span className=\"paragraph-medium-semibold text-gray-600\">{selectedProject?.title}</span>\n <IconChevronDown\n size={20}\n className={`transition-transform duration-200 ${\n activeSection === 'projects' ? 'rotate-180 text-gray-950' : 'text-gray-400'\n }`}\n />\n </div>\n <UsersSelectorPopover projectId={selectedProject?.id} />\n </div>\n {activeSection === 'projects' && (\n <div className=\"flex flex-col p-3 gap-2 border border-gray-200 rounded-lg animate-in fade-in duration-400\">\n <div className=\"flex items-center justify-between\">\n <span className=\"paragraph-small-semibold\">Projetos</span>\n {(onAddProject || onManageProjects) && (\n <div className=\"flex items-center gap-1.5\">\n {canAddProject && onAddProject && (\n <Button variant=\"secondary\" className=\"h-8! paragraph-small-semibold text-gray-950\" onClick={onAddProject}>\n <IconPlus size={16} />\n Adicionar\n </Button>\n )}\n {onManageProjects && (\n <Button variant=\"secondary\" className=\"p-0 size-8\" onClick={onManageProjects}>\n <IconSettings size={18} />\n </Button>\n )}\n </div>\n )}\n </div>\n <Command className=\"gap-2\">\n <CommandInput placeholder=\"Busque aqui\" />\n <CommandList\n className=\"custom-scrollbar paragraph-small-medium text-gray-600 h-[227px]\"\n onScroll={(event) => {\n if (!hasNextPage || isFetchingNextPage || !onLoadMore) return;\n\n const target = event.currentTarget;\n const distanceToBottom = target.scrollHeight - target.scrollTop - target.clientHeight;\n\n if (distanceToBottom <= 80) {\n onLoadMore();\n }\n }}\n >\n <CommandEmpty>Nenhum resultado encontrado.</CommandEmpty>\n {isLoading ? (\n <>\n <ProjectItemSkeleton />\n <ProjectItemSkeleton />\n <ProjectItemSkeleton />\n <ProjectItemSkeleton />\n </>\n ) : (\n projects.map((project) => (\n <CommandItem\n key={project.id}\n value={`${project.title}-${project.id}`}\n keywords={[project.title]}\n className={selectedProject?.id === project.id ? 'bg-gray-50 min-h-[39px]' : 'min-h-[39px]'}\n onSelect={() => { onSelectProject(project); setActiveSection(null); }}\n >\n <UserAvatar\n photo={project.photo}\n name={project.title}\n size={36}\n />\n {project.title}\n </CommandItem>\n ))\n )}\n {isFetchingNextPage && (\n <>\n <ProjectItemSkeleton />\n <ProjectItemSkeleton />\n </>\n )}\n </CommandList>\n </Command>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";AA2BI,SA8HY,UA7HV,KADF;AAzBJ,SAAS,UAAU,QAAQ,iBAA4B;AACvD,SAAS,iBAAiB;AAC1B,SAAS,uBAAuB;AAChC,SAAS,4BAA4B;AACrC,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,QAAQ,SAAS,cAAc,cAAc,aAAa,aAAa,kBAAkB;AAClG,SAAS,iBAAiB,UAAU,oBAAoB;AAexD,SAAS,sBAAsB;AAC7B,SACE,qBAAC,SAAI,WAAU,qCACb;AAAA,wBAAC,SAAI,WAAU,uCAAsC;AAAA,IACrD,oBAAC,SAAI,WAAU,6BAA4B;AAAA,KAC7C;AAEJ;AAEO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,EAAE,WAAW,IAAI,wBAAwB;AAC/C,QAAM,EAAE,UAAU,cAAc,IAAI,gBAAgB,UAAU,QAAQ;AACtE,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,KAAK;AACxD,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAA4B,IAAI;AAC1E,QAAM,WAAW,OAAuB,IAAI;AAC5C,QAAM,EAAE,iBAAiB,iBAAiB,IAAI,qBAAqB;AAEnE,YAAU,MAAM;AACd,QAAI,mBAAmB,eAAe,UAAU;AAC9C,uBAAiB,UAAU;AAC3B,uBAAiB;AAAA,IACnB;AAAA,EACF,GAAG,CAAC,iBAAiB,YAAY,gBAAgB,CAAC;AAGlD,MAAI,eAAe,aAAa,eAAe,MAAM;AACnD,WACE,qBAAC,SAAI,WAAU,qCACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAM;AAAA,UACN,SAAS;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UAEA,+BAAC,SAAI,WAAU,+DAA8D,KAAK,UAC/E;AAAA,wBACC,oBAAC,SAAI,WAAU,6BAA4B,IAE3C,oBAAC,UAAK,WAAU,+EACb,2BAAiB,OACpB;AAAA,YAEF;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN,WAAW,qCAAqC,gBAAgB,6BAA6B,eAAe;AAAA;AAAA,YAC9G;AAAA,aACF;AAAA;AAAA,MACF;AAAA,MACA,oBAAC,SAAI,WAAU,2BACb,8BAAC,wBAAqB,WAAW,iBAAiB,IAAI,GACxD;AAAA,OACF;AAAA,EAEJ;AAGA,SACE,qBAAC,SAAI,WAAU,yBACb;AAAA,yBAAC,SAAI,WAAU,qCACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,SAAS,MAAM,iBAAiB,kBAAkB,aAAa,OAAO,UAAU;AAAA,UAEhF;AAAA,gCAAC,UAAK,WAAU,2CAA2C,2BAAiB,OAAM;AAAA,YAClF;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN,WAAW,qCACT,kBAAkB,aAAa,6BAA6B,eAC9D;AAAA;AAAA,YACF;AAAA;AAAA;AAAA,MACF;AAAA,MACA,oBAAC,wBAAqB,WAAW,iBAAiB,IAAI;AAAA,OACxD;AAAA,IACC,kBAAkB,cACjB,qBAAC,SAAI,WAAU,6FACb;AAAA,2BAAC,SAAI,WAAU,qCACb;AAAA,4BAAC,UAAK,WAAU,4BAA2B,sBAAQ;AAAA,SACjD,gBAAgB,qBAChB,qBAAC,SAAI,WAAU,6BACZ;AAAA,2BAAiB,gBAChB,qBAAC,UAAO,SAAQ,aAAY,WAAU,+CAA8C,SAAS,cAC3F;AAAA,gCAAC,YAAS,MAAM,IAAI;AAAA,YAAE;AAAA,aAExB;AAAA,UAED,oBACC,oBAAC,UAAO,SAAQ,aAAY,WAAU,cAAa,SAAS,kBAC1D,8BAAC,gBAAa,MAAM,IAAI,GAC1B;AAAA,WAEJ;AAAA,SAEJ;AAAA,MACA,qBAAC,WAAQ,WAAU,SACjB;AAAA,4BAAC,gBAAa,aAAY,eAAc;AAAA,QACxC;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,UAAU,CAAC,UAAU;AACnB,kBAAI,CAAC,eAAe,sBAAsB,CAAC,WAAY;AAEvD,oBAAM,SAAS,MAAM;AACrB,oBAAM,mBAAmB,OAAO,eAAe,OAAO,YAAY,OAAO;AAEzE,kBAAI,oBAAoB,IAAI;AAC1B,2BAAW;AAAA,cACb;AAAA,YACF;AAAA,YAEA;AAAA,kCAAC,gBAAa,0CAA4B;AAAA,cACzC,YACC,iCACE;AAAA,oCAAC,uBAAoB;AAAA,gBACrB,oBAAC,uBAAoB;AAAA,gBACrB,oBAAC,uBAAoB;AAAA,gBACrB,oBAAC,uBAAoB;AAAA,iBACvB,IAEA,SAAS,IAAI,CAAC,YACZ;AAAA,gBAAC;AAAA;AAAA,kBAEC,OAAO,GAAG,QAAQ,KAAK,IAAI,QAAQ,EAAE;AAAA,kBACrC,UAAU,CAAC,QAAQ,KAAK;AAAA,kBACxB,WAAW,iBAAiB,OAAO,QAAQ,KAAK,4BAA4B;AAAA,kBAC5E,UAAU,MAAM;AAAE,oCAAgB,OAAO;AAAG,qCAAiB,IAAI;AAAA,kBAAG;AAAA,kBAEpE;AAAA;AAAA,sBAAC;AAAA;AAAA,wBACC,OAAO,QAAQ;AAAA,wBACf,MAAM,QAAQ;AAAA,wBACd,MAAM;AAAA;AAAA,oBACR;AAAA,oBACC,QAAQ;AAAA;AAAA;AAAA,gBAXJ,QAAQ;AAAA,cAYf,CACD;AAAA,cAEF,sBACC,iCACE;AAAA,oCAAC,uBAAoB;AAAA,gBACrB,oBAAC,uBAAoB;AAAA,iBACvB;AAAA;AAAA;AAAA,QAEJ;AAAA,SACF;AAAA,OACF;AAAA,KAEJ;AAEJ;","names":[]}
|
package/dist/infra/api/types.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/infra/api/types.ts"],"sourcesContent":["import z from 'zod';\r\n\r\nexport const PaginationParamsSchema = z.object({\
|
|
1
|
+
{"version":3,"sources":["../../../src/infra/api/types.ts"],"sourcesContent":["import z from 'zod';\r\n\r\nexport const PaginationParamsSchema = z.object({\n page: z.number().min(1).optional().default(1),\n limit: z.number().optional().default(20),\n});\n\r\nexport interface PaginationParams extends z.infer<typeof PaginationParamsSchema> {}\r\n\r\nexport const SearchParamsSchema = z.object({\r\n search: z.string().optional(),\r\n});\r\n\r\nexport interface SearchParams extends z.infer<typeof SearchParamsSchema> {}\r\n\r\nexport const SortParamsSchema = z.object({\r\n sort: z.string().optional(),\r\n});\r\n\r\nexport interface SortParams extends z.infer<typeof SortParamsSchema> {}\r\n\r\nexport interface ApiSuccessResult<T> {\r\n status: 1;\r\n data?: T;\r\n}\r\n\r\nexport interface ApiPaginatedSuccessResult<T> extends Omit<ApiSuccessResult<T>, 'data'> {\r\n data: T[];\r\n total: number;\r\n}\r\n\r\nexport interface ApiErrorResult {\r\n status: 0;\r\n message: string;\r\n}\r\n\r\nexport type ApiActionResult<T> = ApiSuccessResult<T> | ApiErrorResult;\r\nexport type ApiPaginatedActionResult<T> = ApiPaginatedSuccessResult<T> | ApiErrorResult;\r\n\r\nexport interface SuccessResult<T> {\r\n success: true;\r\n data?: T;\r\n total?: number;\r\n}\r\n\r\nexport interface PaginatedSuccessResult<T> extends Omit<SuccessResult<T>, 'data'> {\r\n data: T[];\r\n total: number;\r\n}\r\n\r\nexport interface ErrorResult {\r\n success: false;\r\n error?: string;\r\n}\r\n\r\nexport type ActionResult<T> = SuccessResult<T> | ErrorResult;\r\nexport type PaginatedActionResult<T> = PaginatedSuccessResult<T> | ErrorResult;\r\n\r\nexport class ApiError extends Error {\r\n public readonly code: string;\r\n public readonly statusCode: number;\r\n\r\n constructor(message: string, code: string = 'UNKNOWN_ERROR', statusCode: number = 500) {\r\n super(message);\r\n this.name = 'ApiError';\r\n this.code = code;\r\n this.statusCode = statusCode;\r\n }\r\n\r\n static isApiError(error: unknown): error is ApiError {\r\n return error instanceof ApiError;\r\n }\r\n}\r\n\r\nexport interface ApiResponse<T = unknown> {\r\n status: 0 | 1;\r\n message?: string;\r\n data?: T;\r\n}\r\n"],"mappings":"AAAA,OAAO,OAAO;AAEP,MAAM,yBAAyB,EAAE,OAAO;AAAA,EAC7C,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC;AAAA,EAC5C,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE;AACzC,CAAC;AAIM,MAAM,qBAAqB,EAAE,OAAO;AAAA,EACzC,QAAQ,EAAE,OAAO,EAAE,SAAS;AAC9B,CAAC;AAIM,MAAM,mBAAmB,EAAE,OAAO;AAAA,EACvC,MAAM,EAAE,OAAO,EAAE,SAAS;AAC5B,CAAC;AAyCM,MAAM,iBAAiB,MAAM;AAAA,EAClB;AAAA,EACA;AAAA,EAEhB,YAAY,SAAiB,OAAe,iBAAiB,aAAqB,KAAK;AACrF,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,aAAa;AAAA,EACpB;AAAA,EAEA,OAAO,WAAW,OAAmC;AACnD,WAAO,iBAAiB;AAAA,EAC1B;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Dispatch, RefObject, SetStateAction } from 'react';
|
|
3
|
+
import { Dispatch, RefObject, SetStateAction, UIEvent, useCallback } from 'react';
|
|
4
4
|
import { ADDON_IDS } from '../../modules/subscriptions/constants/addons.constants';
|
|
5
5
|
import { useHasPlanAddon } from '../../modules/plans/hooks/use-has-plan-addon.hook';
|
|
6
6
|
import {
|
|
@@ -24,6 +24,10 @@ interface ProjectSelectorPopoverProps {
|
|
|
24
24
|
titleRef?: RefObject<HTMLSpanElement | null>;
|
|
25
25
|
projects: NavigationProject[];
|
|
26
26
|
selectedProject: NavigationProject | null;
|
|
27
|
+
isLoading?: boolean;
|
|
28
|
+
isFetchingNextPage?: boolean;
|
|
29
|
+
hasNextPage?: boolean;
|
|
30
|
+
onLoadMore?: () => void | Promise<unknown>;
|
|
27
31
|
onSelectProject: (project: NavigationProject) => void;
|
|
28
32
|
onAddProject?: () => void;
|
|
29
33
|
onManageProjects?: () => void;
|
|
@@ -32,10 +36,10 @@ interface ProjectSelectorPopoverProps {
|
|
|
32
36
|
|
|
33
37
|
function ProjectItemSkeleton() {
|
|
34
38
|
return (
|
|
35
|
-
<div className="flex items-center justify-between px-2 py-
|
|
39
|
+
<div className="flex items-center justify-between px-2 py-2 gap-2">
|
|
36
40
|
<div className="flex items-center gap-2 flex-1">
|
|
37
|
-
<div className="skeleton size-
|
|
38
|
-
<div className="skeleton h-
|
|
41
|
+
<div className="skeleton size-9 rounded-lg shrink-0" />
|
|
42
|
+
<div className="skeleton h-4 w-36 rounded" />
|
|
39
43
|
</div>
|
|
40
44
|
<div className="skeleton size-4 rounded-full shrink-0" />
|
|
41
45
|
</div>
|
|
@@ -48,12 +52,26 @@ export function ProjectSelectorPopover({
|
|
|
48
52
|
titleRef,
|
|
49
53
|
projects,
|
|
50
54
|
selectedProject,
|
|
55
|
+
isLoading,
|
|
56
|
+
isFetchingNextPage,
|
|
57
|
+
hasNextPage,
|
|
58
|
+
onLoadMore,
|
|
51
59
|
onSelectProject,
|
|
52
60
|
onAddProject,
|
|
53
61
|
onManageProjects,
|
|
54
62
|
children,
|
|
55
63
|
}: ProjectSelectorPopoverProps) {
|
|
56
64
|
const { hasAddon: canAddProject } = useHasPlanAddon(ADDON_IDS.PROJECTS);
|
|
65
|
+
const handleListScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
|
|
66
|
+
if (!hasNextPage || isFetchingNextPage || !onLoadMore) return;
|
|
67
|
+
|
|
68
|
+
const target = event.currentTarget;
|
|
69
|
+
const distanceToBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
|
|
70
|
+
|
|
71
|
+
if (distanceToBottom <= 80) {
|
|
72
|
+
onLoadMore();
|
|
73
|
+
}
|
|
74
|
+
}, [hasNextPage, isFetchingNextPage, onLoadMore]);
|
|
57
75
|
|
|
58
76
|
return (
|
|
59
77
|
<Popover open={open} onOpenChange={setOpen}>
|
|
@@ -98,8 +116,18 @@ export function ProjectSelectorPopover({
|
|
|
98
116
|
</div>
|
|
99
117
|
<Command className="gap-2">
|
|
100
118
|
<CommandInput placeholder="Busque aqui" />
|
|
101
|
-
<CommandList
|
|
102
|
-
|
|
119
|
+
<CommandList
|
|
120
|
+
className="custom-scrollbar paragraph-small-medium text-gray-600 h-[227px]"
|
|
121
|
+
onScroll={handleListScroll}
|
|
122
|
+
>
|
|
123
|
+
{isLoading ? (
|
|
124
|
+
<>
|
|
125
|
+
<ProjectItemSkeleton />
|
|
126
|
+
<ProjectItemSkeleton />
|
|
127
|
+
<ProjectItemSkeleton />
|
|
128
|
+
<ProjectItemSkeleton />
|
|
129
|
+
</>
|
|
130
|
+
) : projects.length === 0 ? (
|
|
103
131
|
<div className="flex flex-col items-center justify-center gap-2 h-full text-gray-400 py-4">
|
|
104
132
|
<IconFolderOpen size={28} className="text-gray-300" />
|
|
105
133
|
<span className="paragraph-small-medium text-center text-gray-500">
|
|
@@ -118,7 +146,7 @@ export function ProjectSelectorPopover({
|
|
|
118
146
|
key={project.id}
|
|
119
147
|
value={`${project.title}-${project.id}`}
|
|
120
148
|
keywords={[project.title]}
|
|
121
|
-
className={isSelected ? 'bg-gray-50' : ''}
|
|
149
|
+
className={isSelected ? 'bg-gray-50 min-h-[39px]' : 'min-h-[39px]'}
|
|
122
150
|
onSelect={() => onSelectProject(project)}
|
|
123
151
|
>
|
|
124
152
|
<div className="flex items-center justify-between w-full cursor-pointer">
|
|
@@ -126,7 +154,7 @@ export function ProjectSelectorPopover({
|
|
|
126
154
|
<UserAvatar
|
|
127
155
|
photo={project.photo}
|
|
128
156
|
name={project.title}
|
|
129
|
-
size={
|
|
157
|
+
size={36}
|
|
130
158
|
/>
|
|
131
159
|
{project.title}
|
|
132
160
|
</div>
|
|
@@ -137,6 +165,12 @@ export function ProjectSelectorPopover({
|
|
|
137
165
|
</CommandItem>
|
|
138
166
|
);
|
|
139
167
|
})}
|
|
168
|
+
{isFetchingNextPage && (
|
|
169
|
+
<>
|
|
170
|
+
<ProjectItemSkeleton />
|
|
171
|
+
<ProjectItemSkeleton />
|
|
172
|
+
</>
|
|
173
|
+
)}
|
|
140
174
|
</>
|
|
141
175
|
)}
|
|
142
176
|
</CommandList>
|
|
@@ -15,6 +15,9 @@ export interface ProjectSelectorProps {
|
|
|
15
15
|
projects: NavigationProject[];
|
|
16
16
|
selectedProject: NavigationProject | null;
|
|
17
17
|
isLoading?: boolean;
|
|
18
|
+
isFetchingNextPage?: boolean;
|
|
19
|
+
hasNextPage?: boolean;
|
|
20
|
+
onLoadMore?: () => void | Promise<unknown>;
|
|
18
21
|
onSelectProject: (project: NavigationProject) => void;
|
|
19
22
|
onAddProject?: () => void;
|
|
20
23
|
onManageProjects?: () => void;
|
|
@@ -22,9 +25,9 @@ export interface ProjectSelectorProps {
|
|
|
22
25
|
|
|
23
26
|
function ProjectItemSkeleton() {
|
|
24
27
|
return (
|
|
25
|
-
<div className="flex items-center gap-2 p-
|
|
26
|
-
<div className="skeleton size-
|
|
27
|
-
<div className="skeleton h-
|
|
28
|
+
<div className="flex items-center gap-2 p-[8.8px]">
|
|
29
|
+
<div className="skeleton size-9 rounded-lg shrink-0" />
|
|
30
|
+
<div className="skeleton h-4 w-36 rounded" />
|
|
28
31
|
</div>
|
|
29
32
|
);
|
|
30
33
|
}
|
|
@@ -33,6 +36,9 @@ export function ProjectSelector({
|
|
|
33
36
|
projects,
|
|
34
37
|
selectedProject,
|
|
35
38
|
isLoading,
|
|
39
|
+
isFetchingNextPage,
|
|
40
|
+
hasNextPage,
|
|
41
|
+
onLoadMore,
|
|
36
42
|
onSelectProject,
|
|
37
43
|
onAddProject,
|
|
38
44
|
onManageProjects,
|
|
@@ -61,6 +67,10 @@ export function ProjectSelector({
|
|
|
61
67
|
titleRef={titleRef as unknown as RefObject<HTMLSpanElement | null>}
|
|
62
68
|
projects={projects}
|
|
63
69
|
selectedProject={selectedProject}
|
|
70
|
+
isLoading={isLoading}
|
|
71
|
+
isFetchingNextPage={isFetchingNextPage}
|
|
72
|
+
hasNextPage={hasNextPage}
|
|
73
|
+
onLoadMore={onLoadMore}
|
|
64
74
|
onSelectProject={onSelectProject}
|
|
65
75
|
onAddProject={onAddProject}
|
|
66
76
|
onManageProjects={onManageProjects}
|
|
@@ -126,12 +136,26 @@ export function ProjectSelector({
|
|
|
126
136
|
</div>
|
|
127
137
|
<Command className="gap-2">
|
|
128
138
|
<CommandInput placeholder="Busque aqui" />
|
|
129
|
-
<CommandList
|
|
139
|
+
<CommandList
|
|
140
|
+
className="custom-scrollbar paragraph-small-medium text-gray-600 h-[227px]"
|
|
141
|
+
onScroll={(event) => {
|
|
142
|
+
if (!hasNextPage || isFetchingNextPage || !onLoadMore) return;
|
|
143
|
+
|
|
144
|
+
const target = event.currentTarget;
|
|
145
|
+
const distanceToBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
|
|
146
|
+
|
|
147
|
+
if (distanceToBottom <= 80) {
|
|
148
|
+
onLoadMore();
|
|
149
|
+
}
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
130
152
|
<CommandEmpty>Nenhum resultado encontrado.</CommandEmpty>
|
|
131
153
|
{isLoading ? (
|
|
132
154
|
<>
|
|
133
155
|
<ProjectItemSkeleton />
|
|
134
156
|
<ProjectItemSkeleton />
|
|
157
|
+
<ProjectItemSkeleton />
|
|
158
|
+
<ProjectItemSkeleton />
|
|
135
159
|
</>
|
|
136
160
|
) : (
|
|
137
161
|
projects.map((project) => (
|
|
@@ -139,18 +163,24 @@ export function ProjectSelector({
|
|
|
139
163
|
key={project.id}
|
|
140
164
|
value={`${project.title}-${project.id}`}
|
|
141
165
|
keywords={[project.title]}
|
|
142
|
-
className={selectedProject?.id === project.id ? 'bg-gray-50' : ''}
|
|
166
|
+
className={selectedProject?.id === project.id ? 'bg-gray-50 min-h-[39px]' : 'min-h-[39px]'}
|
|
143
167
|
onSelect={() => { onSelectProject(project); setActiveSection(null); }}
|
|
144
168
|
>
|
|
145
169
|
<UserAvatar
|
|
146
170
|
photo={project.photo}
|
|
147
171
|
name={project.title}
|
|
148
|
-
size={
|
|
172
|
+
size={36}
|
|
149
173
|
/>
|
|
150
174
|
{project.title}
|
|
151
175
|
</CommandItem>
|
|
152
176
|
))
|
|
153
177
|
)}
|
|
178
|
+
{isFetchingNextPage && (
|
|
179
|
+
<>
|
|
180
|
+
<ProjectItemSkeleton />
|
|
181
|
+
<ProjectItemSkeleton />
|
|
182
|
+
</>
|
|
183
|
+
)}
|
|
154
184
|
</CommandList>
|
|
155
185
|
</Command>
|
|
156
186
|
</div>
|
package/src/infra/api/types.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import z from 'zod';
|
|
2
2
|
|
|
3
|
-
export const PaginationParamsSchema = z.object({
|
|
4
|
-
page: z.number().min(1).optional().default(
|
|
5
|
-
limit: z.number().optional().default(20),
|
|
6
|
-
});
|
|
3
|
+
export const PaginationParamsSchema = z.object({
|
|
4
|
+
page: z.number().min(1).optional().default(1),
|
|
5
|
+
limit: z.number().optional().default(20),
|
|
6
|
+
});
|
|
7
7
|
|
|
8
8
|
export interface PaginationParams extends z.infer<typeof PaginationParamsSchema> {}
|
|
9
9
|
|