@carlonicora/nextjs-jsonapi 1.0.3 → 1.0.4
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/package.json +2 -1
- package/src/atoms/index.ts +1 -0
- package/src/atoms/recentPagesAtom.ts +10 -0
- package/src/client/context/JsonApiContext.ts +61 -0
- package/src/client/context/JsonApiProvider.tsx +27 -0
- package/src/client/context/index.ts +2 -0
- package/src/client/hooks/index.ts +3 -0
- package/src/client/hooks/useJsonApiGet.ts +188 -0
- package/src/client/hooks/useJsonApiMutation.ts +193 -0
- package/src/client/hooks/useRehydration.ts +47 -0
- package/src/client/index.ts +11 -0
- package/src/client/request.ts +97 -0
- package/src/client/token.ts +10 -0
- package/src/components/containers/PageContainer.tsx +15 -0
- package/src/components/containers/ReactMarkdownContainer.tsx +119 -0
- package/src/components/containers/TabsContainer.tsx +93 -0
- package/src/components/containers/index.ts +3 -0
- package/src/components/contents/AttributeElement.tsx +20 -0
- package/src/components/contents/index.ts +1 -0
- package/src/components/details/AllowedUsersDetails.tsx +23 -0
- package/src/components/details/index.ts +1 -0
- package/src/components/editors/BlockNoteDiffInlineContent.tsx +152 -0
- package/src/components/editors/BlockNoteEditor.tsx +404 -0
- package/src/components/editors/BlockNoteEditorContainer.tsx +13 -0
- package/src/components/editors/BlockNoteEditorFormattingToolbar.tsx +38 -0
- package/src/components/editors/index.ts +1 -0
- package/src/components/errors/ErrorDetails.tsx +41 -0
- package/src/components/errors/errorToast.ts +9 -0
- package/src/components/errors/index.ts +2 -0
- package/src/components/forms/CommonAssociationForm.tsx +162 -0
- package/src/components/forms/CommonDeleter.tsx +94 -0
- package/src/components/forms/CommonEditorButtons.tsx +30 -0
- package/src/components/forms/CommonEditorHeader.tsx +35 -0
- package/src/components/forms/CommonEditorTrigger.tsx +26 -0
- package/src/components/forms/DatePickerPopover.tsx +219 -0
- package/src/components/forms/DateRangeSelector.tsx +110 -0
- package/src/components/forms/FileUploader.tsx +324 -0
- package/src/components/forms/FormCheckbox.tsx +66 -0
- package/src/components/forms/FormContainerGeneric.tsx +39 -0
- package/src/components/forms/FormDate.tsx +247 -0
- package/src/components/forms/FormDateTime.tsx +231 -0
- package/src/components/forms/FormInput.tsx +110 -0
- package/src/components/forms/FormPassword.tsx +54 -0
- package/src/components/forms/FormPlaceAutocomplete.tsx +286 -0
- package/src/components/forms/FormSelect.tsx +72 -0
- package/src/components/forms/FormSlider.tsx +51 -0
- package/src/components/forms/FormSwitch.tsx +25 -0
- package/src/components/forms/FormTextarea.tsx +44 -0
- package/src/components/forms/MultiFileUploader.tsx +107 -0
- package/src/components/forms/PasswordInput.tsx +47 -0
- package/src/components/forms/index.ts +21 -0
- package/src/components/index.ts +11 -0
- package/src/components/navigations/Breadcrumb.tsx +83 -0
- package/src/components/navigations/ContentTitle.tsx +39 -0
- package/src/components/navigations/Header.tsx +27 -0
- package/src/components/navigations/ModeToggleSwitch.tsx +25 -0
- package/src/components/navigations/PageSection.tsx +64 -0
- package/src/components/navigations/RecentPagesNavigator.tsx +52 -0
- package/src/components/navigations/index.ts +6 -0
- package/src/components/pages/PageContainerContentDetails.tsx +76 -0
- package/src/components/pages/PageContentContainer.tsx +31 -0
- package/src/components/pages/index.ts +2 -0
- package/src/components/tables/ContentListTable.tsx +165 -0
- package/src/components/tables/ContentTableSearch.tsx +105 -0
- package/src/components/tables/cells/cell.component.tsx +18 -0
- package/src/components/tables/cells/cell.date.tsx +16 -0
- package/src/components/tables/cells/cell.id.tsx +27 -0
- package/src/components/tables/cells/cell.link.tsx +18 -0
- package/src/components/tables/cells/cell.text.tsx +12 -0
- package/src/components/tables/cells/cell.url.tsx +13 -0
- package/src/components/tables/cells/index.ts +5 -0
- package/src/components/tables/index.ts +3 -0
- package/src/contexts/SharedContext.tsx +35 -0
- package/src/contexts/index.ts +2 -0
- package/src/core/abstracts/AbstractApiData.ts +138 -0
- package/src/core/abstracts/AbstractService.ts +263 -0
- package/src/core/abstracts/index.ts +2 -0
- package/src/core/endpoint/EndpointCreator.ts +97 -0
- package/src/core/endpoint/index.ts +1 -0
- package/src/core/factories/JsonApiDataFactory.ts +12 -0
- package/src/core/factories/RehydrationFactory.ts +30 -0
- package/src/core/factories/index.ts +2 -0
- package/src/core/fields/FieldSelector.ts +15 -0
- package/src/core/fields/index.ts +1 -0
- package/src/core/index.ts +20 -0
- package/src/core/interfaces/ApiData.ts +8 -0
- package/src/core/interfaces/ApiDataInterface.ts +15 -0
- package/src/core/interfaces/ApiRequestDataTypeInterface.ts +14 -0
- package/src/core/interfaces/ApiResponseInterface.ts +17 -0
- package/src/core/interfaces/JsonApiHydratedDataInterface.ts +5 -0
- package/src/core/interfaces/index.ts +5 -0
- package/src/core/registry/DataClassRegistry.ts +51 -0
- package/src/core/registry/ModuleRegistrar.ts +43 -0
- package/src/core/registry/ModuleRegistry.ts +64 -0
- package/src/core/registry/index.ts +3 -0
- package/src/core/utils/index.ts +2 -0
- package/src/core/utils/rehydrate.ts +24 -0
- package/src/core/utils/translateResponse.ts +125 -0
- package/src/features/auth/auth.module.ts +9 -0
- package/src/features/auth/config.ts +57 -0
- package/src/features/auth/data/auth.interface.ts +31 -0
- package/src/features/auth/data/auth.service.ts +159 -0
- package/src/features/auth/data/auth.ts +54 -0
- package/src/features/auth/data/index.ts +3 -0
- package/src/features/auth/index.ts +3 -0
- package/src/features/company/company.module.ts +10 -0
- package/src/features/company/data/company.fields.ts +6 -0
- package/src/features/company/data/company.interface.ts +28 -0
- package/src/features/company/data/company.service.ts +73 -0
- package/src/features/company/data/company.ts +104 -0
- package/src/features/company/data/index.ts +4 -0
- package/src/features/company/index.ts +2 -0
- package/src/features/content/content.module.ts +20 -0
- package/src/features/content/data/content.fields.ts +13 -0
- package/src/features/content/data/content.interface.ts +23 -0
- package/src/features/content/data/content.service.ts +75 -0
- package/src/features/content/data/content.ts +85 -0
- package/src/features/content/data/index.ts +4 -0
- package/src/features/content/index.ts +2 -0
- package/src/features/feature/components/forms/FormFeatures.tsx +149 -0
- package/src/features/feature/components/index.ts +1 -0
- package/src/features/feature/data/feature.interface.ts +9 -0
- package/src/features/feature/data/feature.service.ts +19 -0
- package/src/features/feature/data/feature.ts +33 -0
- package/src/features/feature/data/index.ts +3 -0
- package/src/features/feature/feature.module.ts +10 -0
- package/src/features/feature/index.ts +3 -0
- package/src/features/index.ts +12 -0
- package/src/features/module/data/index.ts +2 -0
- package/src/features/module/data/module.interface.ts +12 -0
- package/src/features/module/data/module.ts +42 -0
- package/src/features/module/index.ts +2 -0
- package/src/features/module/module.module.ts +10 -0
- package/src/features/notification/data/index.ts +4 -0
- package/src/features/notification/data/notification.fields.ts +8 -0
- package/src/features/notification/data/notification.interface.ts +14 -0
- package/src/features/notification/data/notification.service.ts +34 -0
- package/src/features/notification/data/notification.ts +51 -0
- package/src/features/notification/index.ts +2 -0
- package/src/features/notification/notification.module.ts +10 -0
- package/src/features/push/data/index.ts +3 -0
- package/src/features/push/data/push.interface.ts +8 -0
- package/src/features/push/data/push.service.ts +17 -0
- package/src/features/push/data/push.ts +18 -0
- package/src/features/push/index.ts +2 -0
- package/src/features/push/push.module.ts +10 -0
- package/src/features/role/data/index.ts +4 -0
- package/src/features/role/data/role.fields.ts +8 -0
- package/src/features/role/data/role.interface.ts +16 -0
- package/src/features/role/data/role.service.ts +117 -0
- package/src/features/role/data/role.ts +62 -0
- package/src/features/role/index.ts +2 -0
- package/src/features/role/role.module.ts +10 -0
- package/src/features/s3/data/index.ts +3 -0
- package/src/features/s3/data/s3.interface.ts +11 -0
- package/src/features/s3/data/s3.service.ts +30 -0
- package/src/features/s3/data/s3.ts +60 -0
- package/src/features/s3/index.ts +2 -0
- package/src/features/s3/s3.module.ts +10 -0
- package/src/features/search/index.ts +1 -0
- package/src/features/search/interfaces/index.ts +1 -0
- package/src/features/search/interfaces/search.result.interface.ts +3 -0
- package/src/features/user/author.module.ts +10 -0
- package/src/features/user/components/index.ts +2 -0
- package/src/features/user/components/lists/ContributorsList.tsx +41 -0
- package/src/features/user/components/lists/index.ts +1 -0
- package/src/features/user/components/widgets/UserAvatar.tsx +86 -0
- package/src/features/user/components/widgets/index.ts +1 -0
- package/src/features/user/contexts/CurrentUserContext.tsx +156 -0
- package/src/features/user/contexts/index.ts +1 -0
- package/src/features/user/data/index.ts +4 -0
- package/src/features/user/data/user.fields.ts +8 -0
- package/src/features/user/data/user.interface.ts +41 -0
- package/src/features/user/data/user.service.ts +246 -0
- package/src/features/user/data/user.ts +162 -0
- package/src/features/user/index.ts +4 -0
- package/src/features/user/user.module.ts +21 -0
- package/src/hooks/TableGeneratorRegistry.ts +53 -0
- package/src/hooks/index.ts +33 -0
- package/src/hooks/types.ts +35 -0
- package/src/hooks/url.rewriter.ts +22 -0
- package/src/hooks/useCustomD3Graph.tsx +705 -0
- package/src/hooks/useDataListRetriever.ts +349 -0
- package/src/hooks/useDebounce.ts +33 -0
- package/src/hooks/usePageUrlGenerator.ts +50 -0
- package/src/hooks/useTableGenerator.ts +16 -0
- package/src/i18n/config.ts +73 -0
- package/src/i18n/index.ts +18 -0
- package/src/index.ts +16 -0
- package/src/interfaces/breadcrumb.item.data.interface.ts +4 -0
- package/src/interfaces/d3.link.interface.ts +7 -0
- package/src/interfaces/d3.node.interface.ts +12 -0
- package/src/interfaces/index.ts +3 -0
- package/src/permissions/check.ts +127 -0
- package/src/permissions/index.ts +2 -0
- package/src/permissions/types.ts +109 -0
- package/src/roles/config.ts +46 -0
- package/src/roles/index.ts +1 -0
- package/src/server/cache.ts +28 -0
- package/src/server/index.ts +3 -0
- package/src/server/request.ts +113 -0
- package/src/server/token.ts +10 -0
- package/src/shadcnui/custom/kanban.tsx +1001 -0
- package/src/shadcnui/custom/link.tsx +18 -0
- package/src/shadcnui/custom/multi-select.tsx +382 -0
- package/src/shadcnui/index.ts +49 -0
- package/src/shadcnui/ui/accordion.tsx +52 -0
- package/src/shadcnui/ui/alert-dialog.tsx +141 -0
- package/src/shadcnui/ui/alert.tsx +43 -0
- package/src/shadcnui/ui/avatar.tsx +50 -0
- package/src/shadcnui/ui/badge.tsx +40 -0
- package/src/shadcnui/ui/breadcrumb.tsx +115 -0
- package/src/shadcnui/ui/button.tsx +51 -0
- package/src/shadcnui/ui/calendar.tsx +73 -0
- package/src/shadcnui/ui/card.tsx +43 -0
- package/src/shadcnui/ui/carousel.tsx +225 -0
- package/src/shadcnui/ui/chart.tsx +320 -0
- package/src/shadcnui/ui/checkbox.tsx +29 -0
- package/src/shadcnui/ui/collapsible.tsx +11 -0
- package/src/shadcnui/ui/command.tsx +155 -0
- package/src/shadcnui/ui/context-menu.tsx +179 -0
- package/src/shadcnui/ui/dialog.tsx +96 -0
- package/src/shadcnui/ui/drawer.tsx +89 -0
- package/src/shadcnui/ui/dropdown-menu.tsx +205 -0
- package/src/shadcnui/ui/form.tsx +138 -0
- package/src/shadcnui/ui/hover-card.tsx +29 -0
- package/src/shadcnui/ui/input.tsx +21 -0
- package/src/shadcnui/ui/label.tsx +26 -0
- package/src/shadcnui/ui/navigation-menu.tsx +168 -0
- package/src/shadcnui/ui/popover.tsx +33 -0
- package/src/shadcnui/ui/progress.tsx +25 -0
- package/src/shadcnui/ui/radio-group.tsx +37 -0
- package/src/shadcnui/ui/resizable.tsx +47 -0
- package/src/shadcnui/ui/scroll-area.tsx +40 -0
- package/src/shadcnui/ui/select.tsx +164 -0
- package/src/shadcnui/ui/separator.tsx +28 -0
- package/src/shadcnui/ui/sheet.tsx +139 -0
- package/src/shadcnui/ui/sidebar.tsx +677 -0
- package/src/shadcnui/ui/skeleton.tsx +13 -0
- package/src/shadcnui/ui/slider.tsx +25 -0
- package/src/shadcnui/ui/sonner.tsx +25 -0
- package/src/shadcnui/ui/switch.tsx +31 -0
- package/src/shadcnui/ui/table.tsx +120 -0
- package/src/shadcnui/ui/tabs.tsx +55 -0
- package/src/shadcnui/ui/textarea.tsx +24 -0
- package/src/shadcnui/ui/toggle.tsx +39 -0
- package/src/shadcnui/ui/tooltip.tsx +61 -0
- package/src/unified/JsonApiRequest.ts +325 -0
- package/src/unified/index.ts +1 -0
- package/src/utils/blocknote-diff.util.ts +815 -0
- package/src/utils/blocknote-word-diff-renderer.util.ts +413 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/compose-refs.ts +61 -0
- package/src/utils/date-formatter.ts +53 -0
- package/src/utils/exists.ts +7 -0
- package/src/utils/index.ts +15 -0
- package/src/utils/schemas/entity.object.schema.ts +8 -0
- package/src/utils/schemas/index.ts +2 -0
- package/src/utils/schemas/user.object.schema.ts +9 -0
- package/src/utils/table-options.ts +67 -0
- package/src/utils/use-mobile.tsx +21 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
FormControl,
|
|
5
|
+
FormField,
|
|
6
|
+
FormItem,
|
|
7
|
+
FormLabel,
|
|
8
|
+
FormMessage,
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
} from "../../shadcnui";
|
|
15
|
+
|
|
16
|
+
export function FormSelect({
|
|
17
|
+
form,
|
|
18
|
+
id,
|
|
19
|
+
name,
|
|
20
|
+
placeholder,
|
|
21
|
+
disabled,
|
|
22
|
+
values,
|
|
23
|
+
onChange,
|
|
24
|
+
useRows,
|
|
25
|
+
testId,
|
|
26
|
+
}: {
|
|
27
|
+
form: any;
|
|
28
|
+
id: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
placeholder?: string;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
values: { id: string; text: string }[];
|
|
33
|
+
onChange?: (value: string) => void;
|
|
34
|
+
useRows?: boolean;
|
|
35
|
+
testId?: string;
|
|
36
|
+
}) {
|
|
37
|
+
return (
|
|
38
|
+
<div className={`flex w-full flex-col`}>
|
|
39
|
+
<FormField
|
|
40
|
+
control={form.control}
|
|
41
|
+
name={id}
|
|
42
|
+
render={({ field }) => (
|
|
43
|
+
<FormItem className={`flex w-full ${useRows ? `flex-row items-center justify-between gap-x-4` : `flex-col`}`}>
|
|
44
|
+
{name && <FormLabel className={`${useRows ? `min-w-28` : ``}`}>{name}</FormLabel>}
|
|
45
|
+
<Select
|
|
46
|
+
onValueChange={(e) => {
|
|
47
|
+
field.onChange(e);
|
|
48
|
+
if (onChange) onChange(e);
|
|
49
|
+
}}
|
|
50
|
+
defaultValue={field.value}
|
|
51
|
+
data-testid={testId}
|
|
52
|
+
>
|
|
53
|
+
<FormControl className="w-full">
|
|
54
|
+
<SelectTrigger>
|
|
55
|
+
<SelectValue placeholder={placeholder} />
|
|
56
|
+
</SelectTrigger>
|
|
57
|
+
</FormControl>
|
|
58
|
+
<SelectContent>
|
|
59
|
+
{values.map((type: { id: string; text: string }) => (
|
|
60
|
+
<SelectItem key={type.id} value={type.id}>
|
|
61
|
+
{type.text}
|
|
62
|
+
</SelectItem>
|
|
63
|
+
))}
|
|
64
|
+
</SelectContent>
|
|
65
|
+
</Select>
|
|
66
|
+
<FormMessage />
|
|
67
|
+
</FormItem>
|
|
68
|
+
)}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useWatch } from "react-hook-form";
|
|
4
|
+
import { FormControl, FormField, FormItem, FormLabel, FormMessage, Slider } from "../../shadcnui";
|
|
5
|
+
|
|
6
|
+
export function FormSlider({
|
|
7
|
+
form,
|
|
8
|
+
id,
|
|
9
|
+
name,
|
|
10
|
+
disabled,
|
|
11
|
+
showPercentage,
|
|
12
|
+
}: {
|
|
13
|
+
form: any;
|
|
14
|
+
id: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
showPercentage?: boolean;
|
|
19
|
+
}) {
|
|
20
|
+
const value = useWatch({ control: form.control, name: id });
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="flex w-full flex-col">
|
|
24
|
+
<FormField
|
|
25
|
+
control={form.control}
|
|
26
|
+
name={id}
|
|
27
|
+
render={({ field }) => (
|
|
28
|
+
<FormItem className={`${name ? "mb-5" : "mb-1"}`}>
|
|
29
|
+
{name && <FormLabel>{name}</FormLabel>}
|
|
30
|
+
<FormControl>
|
|
31
|
+
<div>
|
|
32
|
+
{showPercentage && (
|
|
33
|
+
<div className="text-muted-foreground mb-2 flex w-full justify-center text-xs">{`${value}%`}</div>
|
|
34
|
+
)}
|
|
35
|
+
<Slider
|
|
36
|
+
onValueChange={(value: number[]) => form.setValue(id, value[0])}
|
|
37
|
+
value={[value]}
|
|
38
|
+
max={100}
|
|
39
|
+
step={5}
|
|
40
|
+
disabled={disabled === true || form.formState.isSubmitting}
|
|
41
|
+
/>
|
|
42
|
+
{/* </div> */}
|
|
43
|
+
</div>
|
|
44
|
+
</FormControl>
|
|
45
|
+
<FormMessage />
|
|
46
|
+
</FormItem>
|
|
47
|
+
)}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { FormControl, FormField, FormItem, FormLabel, FormMessage, Switch } from "../../shadcnui";
|
|
4
|
+
|
|
5
|
+
export function FormSwitch({ form, id, name, disabled }: { form: any; id: string; name?: string; disabled?: boolean }) {
|
|
6
|
+
return (
|
|
7
|
+
<div className="flex w-full flex-col">
|
|
8
|
+
<FormField
|
|
9
|
+
control={form.control}
|
|
10
|
+
name={id}
|
|
11
|
+
render={({ field }) => (
|
|
12
|
+
<FormItem className={`${name ? "mb-5" : "mb-1"}`}>
|
|
13
|
+
<FormControl>
|
|
14
|
+
<div className="flex flex-row gap-x-4">
|
|
15
|
+
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
|
16
|
+
{name && <FormLabel>{name}</FormLabel>}
|
|
17
|
+
</div>
|
|
18
|
+
</FormControl>
|
|
19
|
+
<FormMessage />
|
|
20
|
+
</FormItem>
|
|
21
|
+
)}
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { FormControl, FormField, FormItem, FormLabel, FormMessage, Textarea } from "../../shadcnui";
|
|
4
|
+
import { cn } from "../../utils";
|
|
5
|
+
|
|
6
|
+
export function FormTextarea({
|
|
7
|
+
form,
|
|
8
|
+
id,
|
|
9
|
+
name,
|
|
10
|
+
className,
|
|
11
|
+
placeholder,
|
|
12
|
+
testId,
|
|
13
|
+
}: {
|
|
14
|
+
form: any;
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
testId?: string;
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex w-full flex-col">
|
|
23
|
+
<FormField
|
|
24
|
+
control={form.control}
|
|
25
|
+
name={id}
|
|
26
|
+
render={({ field }) => (
|
|
27
|
+
<FormItem className="mb-5">
|
|
28
|
+
<FormLabel>{name}</FormLabel>
|
|
29
|
+
<FormControl>
|
|
30
|
+
<Textarea
|
|
31
|
+
{...field}
|
|
32
|
+
className={cn("min-h-96 w-full", className)}
|
|
33
|
+
disabled={form.formState.isSubmitting}
|
|
34
|
+
placeholder={placeholder}
|
|
35
|
+
data-testid={testId}
|
|
36
|
+
/>
|
|
37
|
+
</FormControl>
|
|
38
|
+
<FormMessage data-testid={testId ? `${testId}-error` : undefined} />
|
|
39
|
+
</FormItem>
|
|
40
|
+
)}
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { FileIcon, FileImageIcon, FileSpreadsheetIcon, FileTextIcon, UploadIcon, XIcon } from "lucide-react";
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { DropzoneOptions } from "react-dropzone";
|
|
7
|
+
import { cn } from "../../utils";
|
|
8
|
+
import { FileInput, FileUploader } from "./FileUploader";
|
|
9
|
+
|
|
10
|
+
type MultiFileUploaderProps = {
|
|
11
|
+
files: File[];
|
|
12
|
+
setFiles: (files: File[]) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const dropzone = {
|
|
16
|
+
multiple: false,
|
|
17
|
+
maxSize: 100 * 1024 * 1024,
|
|
18
|
+
preventDropOnDocument: false,
|
|
19
|
+
accept: {
|
|
20
|
+
"application/images": [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".docx", ".xslx", ".pdf", ".txt", ".md"],
|
|
21
|
+
},
|
|
22
|
+
} satisfies DropzoneOptions;
|
|
23
|
+
|
|
24
|
+
export default function MultiFileUploader({ files, setFiles }: MultiFileUploaderProps) {
|
|
25
|
+
const uploadFiles = (newFiles: File[] | null) => {
|
|
26
|
+
if (!newFiles) return;
|
|
27
|
+
|
|
28
|
+
setFiles([...files, ...newFiles]);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleRemoveFile = (indexToRemove: number) => {
|
|
32
|
+
setFiles(files.filter((_, index) => index !== indexToRemove));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6">
|
|
37
|
+
<FileUploader
|
|
38
|
+
value={files}
|
|
39
|
+
onValueChange={uploadFiles}
|
|
40
|
+
dropzoneOptions={dropzone}
|
|
41
|
+
className="text-muted-foreground hover:text-primary relative aspect-square h-full cursor-pointer overflow-hidden rounded-lg border"
|
|
42
|
+
>
|
|
43
|
+
<FileInput className={cn("text-muted-foreground flex h-full w-full flex-col items-center justify-center")}>
|
|
44
|
+
<UploadIcon className="my-4 h-8 w-8" />
|
|
45
|
+
</FileInput>
|
|
46
|
+
</FileUploader>
|
|
47
|
+
{files.map((file, index) => (
|
|
48
|
+
<FilePreviewItem key={file.name + "-" + index} file={file} onRemoveClick={() => handleRemoveFile(index)} />
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const FilePreviewItem = ({ file, onRemoveClick }: { file: File; onRemoveClick: () => void }) => {
|
|
55
|
+
const [objectUrl, setObjectUrl] = useState<string | null>(null);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (file.type.startsWith("image/")) {
|
|
59
|
+
const url = URL.createObjectURL(file);
|
|
60
|
+
setObjectUrl(url);
|
|
61
|
+
return () => URL.revokeObjectURL(url);
|
|
62
|
+
}
|
|
63
|
+
setObjectUrl(null);
|
|
64
|
+
}, [file]);
|
|
65
|
+
|
|
66
|
+
const getFileIcon = () => {
|
|
67
|
+
const extension = file.name.split(".").pop()?.toLowerCase();
|
|
68
|
+
if (file.type.startsWith("image/")) {
|
|
69
|
+
return <FileImageIcon className="text-muted h-10 w-10 sm:h-12 sm:w-12" />;
|
|
70
|
+
}
|
|
71
|
+
if (file.type === "application/pdf") {
|
|
72
|
+
return <FileTextIcon className="text-destructive h-10 w-10 sm:h-12 sm:w-12" />;
|
|
73
|
+
}
|
|
74
|
+
if (file.type.includes("wordprocessingml") || extension === "docx" || extension === "doc") {
|
|
75
|
+
return <FileTextIcon className="text-primary h-10 w-10 sm:h-12 sm:w-12" />;
|
|
76
|
+
}
|
|
77
|
+
if (file.type.includes("spreadsheetml") || extension === "xlsx" || extension === "xls") {
|
|
78
|
+
return <FileSpreadsheetIcon className="text-secondary h-10 w-10 sm:h-12 sm:w-12" />;
|
|
79
|
+
}
|
|
80
|
+
if (file.type.startsWith("text/")) {
|
|
81
|
+
return <FileTextIcon className="h-10 w-10 sm:h-12 sm:w-12" />;
|
|
82
|
+
}
|
|
83
|
+
return <FileIcon className="text-muted h-10 w-10 sm:h-12 sm:w-12" />;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className="text-muted-foreground group relative aspect-square h-full overflow-hidden rounded-lg border">
|
|
88
|
+
<button
|
|
89
|
+
onClick={onRemoveClick}
|
|
90
|
+
className="absolute top-1.5 right-1.5 z-20 rounded-full bg-black/60 p-1 text-white opacity-0 transition-all duration-200 ease-in-out group-hover:opacity-100 hover:bg-red-600 focus:outline-none"
|
|
91
|
+
aria-label="Remove file"
|
|
92
|
+
>
|
|
93
|
+
<XIcon className="h-3.5 w-3.5" />
|
|
94
|
+
</button>
|
|
95
|
+
{objectUrl ? (
|
|
96
|
+
<Image src={objectUrl} alt={file.name} className="h-full w-full object-cover" width={200} height={200} />
|
|
97
|
+
) : (
|
|
98
|
+
<div className="bg-muted/30 flex h-full w-full flex-col items-center justify-center p-2">
|
|
99
|
+
{getFileIcon()}
|
|
100
|
+
<span className="text-foreground/80 mt-2 w-full truncate px-1 text-center text-[10px] sm:text-xs">
|
|
101
|
+
{file.name}
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { Button, Input } from "../../shadcnui";
|
|
6
|
+
import { cn } from "../../utils";
|
|
7
|
+
|
|
8
|
+
export interface PasswordInputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
9
|
+
|
|
10
|
+
const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(({ className, type, ...props }, ref) => {
|
|
11
|
+
const [showPassword, setShowPassword] = React.useState(false);
|
|
12
|
+
const disabled = props.value === "" || props.value === undefined || props.disabled;
|
|
13
|
+
const t = useTranslations();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="relative">
|
|
17
|
+
<Input type={showPassword ? "text" : "password"} className={cn("", className)} ref={ref} {...props} />
|
|
18
|
+
<Button
|
|
19
|
+
type="button"
|
|
20
|
+
variant="ghost"
|
|
21
|
+
size="sm"
|
|
22
|
+
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
|
23
|
+
onClick={() => setShowPassword((prev) => !prev)}
|
|
24
|
+
disabled={disabled}
|
|
25
|
+
>
|
|
26
|
+
{showPassword && !disabled ? (
|
|
27
|
+
<EyeIcon className="h-4 w-4" aria-hidden="true" />
|
|
28
|
+
) : (
|
|
29
|
+
<EyeOffIcon className="h-4 w-4" aria-hidden="true" />
|
|
30
|
+
)}
|
|
31
|
+
<span className="sr-only">{showPassword ? t(`generic.hide_password`) : t(`generic.show_password`)}</span>
|
|
32
|
+
</Button>
|
|
33
|
+
|
|
34
|
+
<style>{`
|
|
35
|
+
.hide-password-toggle::-ms-reveal,
|
|
36
|
+
.hide-password-toggle::-ms-clear {
|
|
37
|
+
visibility: hidden;
|
|
38
|
+
pointer-events: none;
|
|
39
|
+
display: none;
|
|
40
|
+
}
|
|
41
|
+
`}</style>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
PasswordInput.displayName = "PasswordInput";
|
|
46
|
+
|
|
47
|
+
export { PasswordInput };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export * from "./CommonAssociationForm";
|
|
2
|
+
export * from "./CommonDeleter";
|
|
3
|
+
export * from "./CommonEditorButtons";
|
|
4
|
+
export * from "./CommonEditorHeader";
|
|
5
|
+
export * from "./CommonEditorTrigger";
|
|
6
|
+
export * from "./DatePickerPopover";
|
|
7
|
+
export * from "./DateRangeSelector";
|
|
8
|
+
export * from "./FileUploader";
|
|
9
|
+
export * from "./FormCheckbox";
|
|
10
|
+
export * from "./FormContainerGeneric";
|
|
11
|
+
export * from "./FormDate";
|
|
12
|
+
export * from "./FormDateTime";
|
|
13
|
+
export * from "./FormInput";
|
|
14
|
+
export * from "./FormPassword";
|
|
15
|
+
export * from "./FormPlaceAutocomplete";
|
|
16
|
+
export * from "./FormSelect";
|
|
17
|
+
export * from "./FormSlider";
|
|
18
|
+
export * from "./FormSwitch";
|
|
19
|
+
export * from "./FormTextarea";
|
|
20
|
+
export * from "./MultiFileUploader";
|
|
21
|
+
export * from "./PasswordInput";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./containers";
|
|
2
|
+
export * from "./contents";
|
|
3
|
+
export * from "./details";
|
|
4
|
+
export * from "./editors";
|
|
5
|
+
export * from "./errors";
|
|
6
|
+
export * from "./forms";
|
|
7
|
+
export * from "./navigations";
|
|
8
|
+
export * from "./pages";
|
|
9
|
+
export * from "./tables";
|
|
10
|
+
|
|
11
|
+
export * from "../features/user/components";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { Fragment, useState } from "react";
|
|
5
|
+
import { usePageUrlGenerator } from "../../hooks";
|
|
6
|
+
import { BreadcrumbItemData } from "../../interfaces";
|
|
7
|
+
import {
|
|
8
|
+
BreadcrumbEllipsis,
|
|
9
|
+
BreadcrumbItem,
|
|
10
|
+
BreadcrumbList,
|
|
11
|
+
BreadcrumbSeparator,
|
|
12
|
+
DropdownMenu,
|
|
13
|
+
DropdownMenuContent,
|
|
14
|
+
DropdownMenuItem,
|
|
15
|
+
DropdownMenuTrigger,
|
|
16
|
+
Link,
|
|
17
|
+
Breadcrumb as UIBreadcrumb,
|
|
18
|
+
} from "../../shadcnui";
|
|
19
|
+
|
|
20
|
+
type BreadcrumbProps = { items: BreadcrumbItemData[] };
|
|
21
|
+
|
|
22
|
+
const ITEMS_TO_DISPLAY = 3;
|
|
23
|
+
|
|
24
|
+
export function Breadcrumb({ items }: BreadcrumbProps) {
|
|
25
|
+
const generateUrl = usePageUrlGenerator();
|
|
26
|
+
const t = useTranslations();
|
|
27
|
+
|
|
28
|
+
const [open, setOpen] = useState<boolean>(false);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<UIBreadcrumb>
|
|
32
|
+
<BreadcrumbList>
|
|
33
|
+
<BreadcrumbItem>
|
|
34
|
+
<Link href={generateUrl({ page: `/` })}>{t(`generic.home`)}</Link>
|
|
35
|
+
</BreadcrumbItem>
|
|
36
|
+
{items.length > 0 && <BreadcrumbSeparator />}
|
|
37
|
+
|
|
38
|
+
{items.length > ITEMS_TO_DISPLAY ? (
|
|
39
|
+
<>
|
|
40
|
+
<BreadcrumbItem>
|
|
41
|
+
{items[0].href ? <Link href={items[0].href}>{items[0].name}</Link> : <>{items[0].name}</>}
|
|
42
|
+
</BreadcrumbItem>
|
|
43
|
+
<BreadcrumbSeparator />
|
|
44
|
+
<BreadcrumbItem>
|
|
45
|
+
<DropdownMenu open={open} onOpenChange={setOpen}>
|
|
46
|
+
<DropdownMenuTrigger className="flex items-center gap-1" aria-label="Toggle menu">
|
|
47
|
+
<BreadcrumbEllipsis className="h-4 w-4" />
|
|
48
|
+
</DropdownMenuTrigger>
|
|
49
|
+
<DropdownMenuContent align="start">
|
|
50
|
+
{items.slice(1, -ITEMS_TO_DISPLAY + 1).map((item, index) => (
|
|
51
|
+
<DropdownMenuItem key={index}>
|
|
52
|
+
<Link href={item.href ? item.href : "#"}>{item.name}</Link>
|
|
53
|
+
</DropdownMenuItem>
|
|
54
|
+
))}
|
|
55
|
+
</DropdownMenuContent>
|
|
56
|
+
</DropdownMenu>
|
|
57
|
+
</BreadcrumbItem>
|
|
58
|
+
<BreadcrumbSeparator />
|
|
59
|
+
{items.slice(-ITEMS_TO_DISPLAY + 1).map((item, index) => (
|
|
60
|
+
<Fragment key={index}>
|
|
61
|
+
<BreadcrumbItem>
|
|
62
|
+
{item.href ? <Link href={item.href}>{item.name}</Link> : <>{item.name}</>}
|
|
63
|
+
</BreadcrumbItem>
|
|
64
|
+
{index < items.slice(-ITEMS_TO_DISPLAY + 1).length - 1 && <BreadcrumbSeparator />}
|
|
65
|
+
</Fragment>
|
|
66
|
+
))}
|
|
67
|
+
</>
|
|
68
|
+
) : (
|
|
69
|
+
<>
|
|
70
|
+
{items.map((item, index) => (
|
|
71
|
+
<Fragment key={index}>
|
|
72
|
+
<BreadcrumbItem>
|
|
73
|
+
{item.href ? <Link href={item.href}>{item.name}</Link> : <>{item.name}</>}
|
|
74
|
+
</BreadcrumbItem>
|
|
75
|
+
{index < items.length - 1 && <BreadcrumbSeparator />}
|
|
76
|
+
</Fragment>
|
|
77
|
+
))}
|
|
78
|
+
</>
|
|
79
|
+
)}
|
|
80
|
+
</BreadcrumbList>
|
|
81
|
+
</UIBreadcrumb>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ReactNode, useEffect, useState } from "react";
|
|
4
|
+
import { cn } from "../../utils";
|
|
5
|
+
|
|
6
|
+
type TitleProps = {
|
|
7
|
+
type?: string | string[];
|
|
8
|
+
element?: string;
|
|
9
|
+
functions?: ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function ContentTitle({ type, element, functions, className }: TitleProps) {
|
|
14
|
+
const [clientFunctions, setClientFunctions] = useState<ReactNode>(null);
|
|
15
|
+
const [isClient, setIsClient] = useState(false);
|
|
16
|
+
|
|
17
|
+
// Defer function rendering to client-side only to prevent hydration mismatches
|
|
18
|
+
// caused by Radix UI's dynamic ID generation in Dialog/AlertDialog components
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setIsClient(true);
|
|
21
|
+
setClientFunctions(functions);
|
|
22
|
+
}, [functions]);
|
|
23
|
+
|
|
24
|
+
if (!element) return null;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className={cn(`mb-4 flex w-full flex-col`, className)}>
|
|
28
|
+
{(type || isClient) && (
|
|
29
|
+
<div className="flex flex-row items-center justify-between gap-x-4">
|
|
30
|
+
{type && <div className={`text-muted-foreground text-xl font-light`}>{type}</div>}
|
|
31
|
+
{isClient && clientFunctions && (
|
|
32
|
+
<div className="flex flex-row items-center justify-start">{clientFunctions}</div>
|
|
33
|
+
)}
|
|
34
|
+
</div>
|
|
35
|
+
)}
|
|
36
|
+
<div className={`text-primary w-full text-3xl font-semibold`}>{element}</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSharedContext } from "../../contexts/SharedContext";
|
|
4
|
+
import { SidebarTrigger } from "../../shadcnui";
|
|
5
|
+
import { Breadcrumb } from "./Breadcrumb";
|
|
6
|
+
|
|
7
|
+
type HeaderProps = {
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function Header({ children }: HeaderProps) {
|
|
12
|
+
const { breadcrumbs } = useSharedContext();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<header className={`sticky top-0 z-10 flex h-12 flex-col items-center justify-start gap-x-4 border-b`}>
|
|
16
|
+
<div className="bg-sidebar flex h-12 w-full flex-row items-center justify-between pl-2 pr-4">
|
|
17
|
+
<SidebarTrigger aria-label="Toggle sidebar" />
|
|
18
|
+
<div className="flex w-full flex-row items-center justify-start">
|
|
19
|
+
<Breadcrumb items={breadcrumbs} />
|
|
20
|
+
</div>
|
|
21
|
+
<div className="flex w-64 flex-row items-center justify-end gap-x-4 whitespace-nowrap">
|
|
22
|
+
{children ? children : null}
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</header>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { MoonIcon, SunIcon } from "lucide-react";
|
|
4
|
+
import { useTheme } from "next-themes";
|
|
5
|
+
import { Switch } from "../../shadcnui";
|
|
6
|
+
|
|
7
|
+
export function ModeToggleSwitch() {
|
|
8
|
+
const { theme, setTheme } = useTheme();
|
|
9
|
+
|
|
10
|
+
const handleToggle = () => {
|
|
11
|
+
setTheme(theme === "light" ? "dark" : "light");
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex items-center">
|
|
16
|
+
<Switch checked={theme === "dark"} onCheckedChange={handleToggle} className="relative">
|
|
17
|
+
{theme === "dark" ? (
|
|
18
|
+
<MoonIcon className="text-primary-foreground h-4 w-4" />
|
|
19
|
+
) : (
|
|
20
|
+
<SunIcon className="text-primary h-4 w-4" />
|
|
21
|
+
)}
|
|
22
|
+
</Switch>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
|
|
4
|
+
import { ReactNode, useEffect, useState } from "react";
|
|
5
|
+
import { v4 } from "uuid";
|
|
6
|
+
|
|
7
|
+
type PageSectionProps = {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
title?: string;
|
|
10
|
+
options?: ReactNode[];
|
|
11
|
+
open?: boolean;
|
|
12
|
+
small?: boolean;
|
|
13
|
+
onToggle?: (isOpen: boolean) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function PageSection({ children, title, options, open, small, onToggle }: PageSectionProps) {
|
|
17
|
+
const [isOpen, setIsOpen] = useState<boolean>(open ?? true);
|
|
18
|
+
const [shouldRender, setShouldRender] = useState<boolean>(open ?? true);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (onToggle) {
|
|
22
|
+
onToggle(isOpen);
|
|
23
|
+
}
|
|
24
|
+
}, [isOpen]);
|
|
25
|
+
|
|
26
|
+
const toggleOpen = () => setIsOpen(!isOpen);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (isOpen) {
|
|
30
|
+
setShouldRender(true);
|
|
31
|
+
} else {
|
|
32
|
+
const timer = setTimeout(() => setShouldRender(false), 300);
|
|
33
|
+
return () => clearTimeout(timer);
|
|
34
|
+
}
|
|
35
|
+
}, [isOpen]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<section
|
|
39
|
+
id={title ? title.toLowerCase().replaceAll(" ", "") : v4()}
|
|
40
|
+
className={`${isOpen ? "mb-4" : "my-0"} flex w-full scroll-mt-40 flex-col`}
|
|
41
|
+
>
|
|
42
|
+
{title && (
|
|
43
|
+
<div
|
|
44
|
+
className={`${isOpen ? "mb-4" : "mb-0"} flex w-full justify-between border-b ${small ? `border-muted` : `border-primary`} pb-1`}
|
|
45
|
+
>
|
|
46
|
+
<div className="flex w-full cursor-pointer items-center justify-start gap-x-2" onClick={toggleOpen}>
|
|
47
|
+
{isOpen ? (
|
|
48
|
+
<ChevronDownIcon className={`text-primary h-4 w-4`} />
|
|
49
|
+
) : (
|
|
50
|
+
<ChevronRightIcon className="text-primary h-4 w-4" />
|
|
51
|
+
)}
|
|
52
|
+
<h2 className={`flex w-full ${small === true ? `text-sm` : `text-lg`} text-primary font-semibold`}>
|
|
53
|
+
{title}
|
|
54
|
+
</h2>
|
|
55
|
+
</div>
|
|
56
|
+
{options && <div className="flex gap-2">{options}</div>}
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
<div className={`overflow-hidden transition-all duration-300 ${isOpen ? "" : "max-h-0"}`}>
|
|
60
|
+
{shouldRender && children}
|
|
61
|
+
</div>
|
|
62
|
+
</section>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useAtomValue } from "jotai";
|
|
4
|
+
import { HistoryIcon } from "lucide-react";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
import { recentPagesAtom } from "../../atoms";
|
|
7
|
+
import {
|
|
8
|
+
DropdownMenu,
|
|
9
|
+
DropdownMenuContent,
|
|
10
|
+
DropdownMenuItem,
|
|
11
|
+
DropdownMenuLabel,
|
|
12
|
+
DropdownMenuSeparator,
|
|
13
|
+
DropdownMenuTrigger,
|
|
14
|
+
Link,
|
|
15
|
+
useSidebar,
|
|
16
|
+
} from "../../shadcnui";
|
|
17
|
+
|
|
18
|
+
export function RecentPagesNavigator() {
|
|
19
|
+
const recentPages = useAtomValue(recentPagesAtom);
|
|
20
|
+
const t = useTranslations();
|
|
21
|
+
const { state } = useSidebar();
|
|
22
|
+
|
|
23
|
+
if (recentPages.length === 0) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<DropdownMenu>
|
|
29
|
+
<DropdownMenuTrigger asChild>
|
|
30
|
+
<div className="flex w-full cursor-pointer items-center gap-2">
|
|
31
|
+
{state === "collapsed" ? <HistoryIcon className="h-4 w-4" /> : <span>{t(`generic.recent_pages`)}</span>}
|
|
32
|
+
</div>
|
|
33
|
+
</DropdownMenuTrigger>
|
|
34
|
+
<DropdownMenuContent align="start" className="w-96">
|
|
35
|
+
<DropdownMenuLabel>{t(`generic.recent_pages`)}</DropdownMenuLabel>
|
|
36
|
+
<DropdownMenuSeparator />
|
|
37
|
+
{recentPages.map((page, index) => (
|
|
38
|
+
<DropdownMenuItem key={`${page.url}-${index}`} asChild>
|
|
39
|
+
<Link href={page.url} className="flex items-center gap-2">
|
|
40
|
+
<div className="flex flex-col">
|
|
41
|
+
<div className="truncate text-sm">{page.title}</div>
|
|
42
|
+
<div className="text-muted-foreground text-xs font-normal">
|
|
43
|
+
{t(`types.${page.moduleType}`, { count: 1 })}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</Link>
|
|
47
|
+
</DropdownMenuItem>
|
|
48
|
+
))}
|
|
49
|
+
</DropdownMenuContent>
|
|
50
|
+
</DropdownMenu>
|
|
51
|
+
);
|
|
52
|
+
}
|