@js-empire/emperor-ui 1.2.3 → 1.2.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.
Files changed (157) hide show
  1. package/.cursor/rules/code-conventions.mdc +50 -0
  2. package/README.md +0 -15
  3. package/dist/emperor-ui.js +119 -71
  4. package/dist/emperor-ui.umd.cjs +27 -13
  5. package/dist/globals.css +1 -1
  6. package/dist/index-BXtdEByK.js +5 -0
  7. package/dist/index-CDB93OLO.js +55965 -0
  8. package/dist/index-CYORMghp.js +290 -0
  9. package/dist/index.d.ts +334 -33
  10. package/dist/src-UW24ZMRV-C1Pn8-w8.js +5 -0
  11. package/package.json +32 -2
  12. package/src/animations/blink.ts +26 -0
  13. package/src/animations/floating.ts +12 -0
  14. package/src/animations/index.ts +2 -0
  15. package/src/components/atoms/brand/brand.tsx +1 -1
  16. package/src/components/atoms/color-picker/color-picker.tsx +13 -0
  17. package/src/components/atoms/color-picker/free-color-picker.tsx +60 -0
  18. package/src/components/atoms/color-picker/index.ts +3 -0
  19. package/src/components/atoms/color-picker/preset-color-picker.tsx +64 -0
  20. package/src/components/atoms/color-picker/stories/color-picker.stories.tsx +49 -0
  21. package/src/components/atoms/color-picker/styles/color-picker.css +23 -0
  22. package/src/components/atoms/copy-button/copy-button.tsx +73 -0
  23. package/src/components/atoms/copy-button/index.ts +1 -0
  24. package/src/components/atoms/copy-button/stories/copy-button.stories.tsx +21 -0
  25. package/src/components/atoms/field/field.stories.tsx +27 -0
  26. package/src/components/atoms/field/field.tsx +11 -0
  27. package/src/components/atoms/field/index.ts +1 -0
  28. package/src/components/atoms/field/styles/classes.ts +9 -0
  29. package/src/components/atoms/field/styles/index.ts +1 -0
  30. package/src/components/atoms/filter/filter.tsx +92 -0
  31. package/src/components/atoms/filter/index.ts +3 -0
  32. package/src/components/atoms/filter/stories/filter.stories.tsx +97 -0
  33. package/src/components/atoms/filter/styles/classes.ts +20 -0
  34. package/src/components/atoms/filter/styles/index.ts +1 -0
  35. package/src/components/atoms/filter/units/autocomplete-filter.tsx +39 -0
  36. package/src/components/atoms/filter/units/checkbox-filter.tsx +32 -0
  37. package/src/components/atoms/filter/units/checkbox-group-filter.tsx +37 -0
  38. package/src/components/atoms/filter/units/date-filter.tsx +50 -0
  39. package/src/components/atoms/filter/units/index.ts +9 -0
  40. package/src/components/atoms/filter/units/numeric-filter.tsx +36 -0
  41. package/src/components/atoms/filter/units/range-filter.tsx +36 -0
  42. package/src/components/atoms/filter/units/search-filter.tsx +52 -0
  43. package/src/components/atoms/filter/units/select-filter.tsx +49 -0
  44. package/src/components/atoms/filter/units/switch-filter.tsx +33 -0
  45. package/src/components/atoms/index.ts +5 -0
  46. package/src/components/atoms/theme-switch/index.ts +1 -0
  47. package/src/components/atoms/theme-switch/styles/classes.ts +16 -0
  48. package/src/components/atoms/theme-switch/styles/index.ts +1 -0
  49. package/src/components/atoms/theme-switch/theme-switch.stories.tsx +26 -0
  50. package/src/components/atoms/theme-switch/theme-switch.tsx +54 -0
  51. package/src/components/atoms/uploader/avatar-label.tsx +3 -1
  52. package/src/components/atoms/uploader/stories/uploader.stories.tsx +1 -1
  53. package/src/components/atoms/uploader/upload-file-error-box.tsx +1 -1
  54. package/src/components/atoms/uploader/upload-file-input.tsx +1 -1
  55. package/src/components/atoms/uploader/upload-file-label.tsx +2 -1
  56. package/src/components/atoms/uploader/upload-file-listing.tsx +2 -1
  57. package/src/components/atoms/uploader/view-image-modal.tsx +2 -1
  58. package/src/components/molecules/index.ts +0 -1
  59. package/src/components/molecules/item-card/index.ts +6 -0
  60. package/src/components/molecules/item-card/item-actions-dropdown.tsx +57 -0
  61. package/src/components/molecules/item-card/item-banner.tsx +22 -0
  62. package/src/components/molecules/item-card/item-card-body.tsx +68 -0
  63. package/src/components/molecules/item-card/item-card-footer.tsx +55 -0
  64. package/src/components/molecules/item-card/item-card-header.tsx +61 -0
  65. package/src/components/molecules/item-card/item-card.tsx +83 -3
  66. package/src/components/molecules/item-card/loading-item.tsx +88 -0
  67. package/src/components/molecules/item-card/stories/item-card.stories.tsx +182 -0
  68. package/src/components/molecules/item-card/styles/classes.ts +138 -0
  69. package/src/components/molecules/item-card/styles/index.ts +1 -0
  70. package/src/components/molecules/nav-bar/sub-items-box.tsx +2 -1
  71. package/src/components/molecules/scaffold/index.ts +1 -0
  72. package/src/components/molecules/scaffold/scaffold.tsx +4 -17
  73. package/src/components/molecules/scaffold/styles/index.ts +1 -0
  74. package/src/components/molecules/scaffold/styles/scaffold-classes.ts +10 -0
  75. package/src/components/molecules/side-bar/compact-side-bar.tsx +3 -1
  76. package/src/components/molecules/side-bar/side-bar-drawer.tsx +6 -17
  77. package/src/components/molecules/side-bar/side-bar.stories.tsx +1 -1
  78. package/src/components/organisms/filters/filters.stories.tsx +32 -0
  79. package/src/components/organisms/filters/filters.tsx +36 -0
  80. package/src/components/organisms/filters/index.ts +1 -0
  81. package/src/components/organisms/filters/styles/classes.ts +9 -0
  82. package/src/components/organisms/filters/styles/index.ts +1 -0
  83. package/src/components/organisms/footer/copy-rights-box.tsx +1 -1
  84. package/src/components/organisms/footer/footer.tsx +1 -1
  85. package/src/components/organisms/footer/policies-box.tsx +2 -1
  86. package/src/components/organisms/footer/quick-links-box.tsx +2 -1
  87. package/src/components/organisms/footer/social-links-box.tsx +2 -1
  88. package/src/components/organisms/footer/stories/footer.stories.tsx +1 -1
  89. package/src/components/organisms/header/header.tsx +1 -8
  90. package/src/components/organisms/index.ts +1 -0
  91. package/src/components/organisms/listings/empty-listings.tsx +80 -0
  92. package/src/components/organisms/listings/index.ts +2 -0
  93. package/src/components/organisms/listings/listings.tsx +90 -9
  94. package/src/components/organisms/listings/stories/grid-listings.stories.tsx +153 -0
  95. package/src/components/organisms/listings/stories/list-listings.stories.tsx +171 -0
  96. package/src/components/organisms/listings/styles/classes.ts +41 -3
  97. package/src/constants/animations.ts +14 -0
  98. package/src/constants/card.tsx +26 -0
  99. package/src/constants/defaults.ts +1 -16
  100. package/src/constants/index.ts +2 -0
  101. package/src/hooks/index.ts +3 -0
  102. package/src/hooks/use-filters.ts +20 -0
  103. package/src/hooks/use-search-params-handler.tsx +186 -0
  104. package/src/hooks/use-uploader.tsx +1 -1
  105. package/src/hooks/use-window-size.tsx +53 -0
  106. package/src/i18n/locales/atoms/ar.ts +3 -0
  107. package/src/i18n/locales/atoms/en.ts +3 -0
  108. package/src/i18n/locales/organisms/ar.ts +7 -1
  109. package/src/i18n/locales/organisms/en.ts +7 -1
  110. package/src/mocks/constants.ts +103 -0
  111. package/src/mocks/index.ts +2 -0
  112. package/src/mocks/listings.tsx +154 -0
  113. package/src/mocks/types.ts +64 -0
  114. package/src/providers/config-provider.tsx +0 -8
  115. package/src/providers/emperor-ui-provider.tsx +16 -5
  116. package/src/providers/index.ts +1 -0
  117. package/src/providers/theme-provider.tsx +16 -0
  118. package/src/providers/uploader-provider.tsx +1 -1
  119. package/src/styles/hero.ts +1 -1
  120. package/src/styles/index.css +23 -0
  121. package/src/types/components/atoms/color-picker/color-picker.ts +12 -0
  122. package/src/types/components/atoms/color-picker/index.ts +1 -0
  123. package/src/types/components/atoms/field/field.ts +9 -0
  124. package/src/types/components/atoms/field/index.ts +1 -0
  125. package/src/types/components/atoms/filter/filter.ts +43 -0
  126. package/src/types/components/atoms/filter/index.ts +2 -0
  127. package/src/types/components/atoms/filter/select-filter.ts +8 -0
  128. package/src/types/components/atoms/index.ts +3 -0
  129. package/src/types/components/atoms/uploader.ts +1 -1
  130. package/src/types/components/index.ts +1 -0
  131. package/src/types/components/molecules/index.ts +1 -1
  132. package/src/types/components/molecules/item-card/item-card.ts +50 -0
  133. package/src/types/components/molecules/listings/listings.ts +21 -5
  134. package/src/types/components/molecules/side-bar/side-bar.ts +1 -1
  135. package/src/types/components/molecules/theme-switch/index.ts +1 -0
  136. package/src/types/components/molecules/theme-switch/theme-switch.ts +9 -0
  137. package/src/types/components/organisms/filters/filters.ts +11 -0
  138. package/src/types/components/organisms/filters/index.ts +1 -0
  139. package/src/types/components/organisms/index.ts +1 -0
  140. package/src/types/context/config.ts +3 -4
  141. package/src/types/context/index.ts +0 -1
  142. package/src/types/context/localization.ts +1 -0
  143. package/src/types/shared/components.ts +3 -0
  144. package/src/utils/uploader.ts +1 -1
  145. package/dist/features-animation-w9dWMd15.js +0 -1938
  146. package/dist/index-BY47HgaP.js +0 -26533
  147. package/dist/index-CN4cJ1N7.js +0 -1630
  148. package/dist/index-Cr1mc-d4.js +0 -5
  149. package/dist/src-UW24ZMRV-nsR4cpiy.js +0 -5
  150. package/src/components/molecules/filter/filter.tsx +0 -6
  151. package/src/components/molecules/filter/index.ts +0 -1
  152. package/src/components/organisms/listings/stories/listings.stories.tsx +0 -30
  153. package/src/main.tsx +0 -3
  154. package/src/mocks/listings.ts +0 -200
  155. package/src/types/components/molecules/filter/filter.ts +0 -9
  156. package/src/types/components/molecules/filter/index.ts +0 -1
  157. package/src/types/context/theme.ts +0 -17
@@ -1,36 +1,23 @@
1
1
  import { useEmperorUI } from "@/hooks";
2
2
  import type { ScaffoldProps } from "@/types";
3
3
  import { cn } from "@/utils";
4
- import { cva } from "class-variance-authority";
5
4
  import { forwardRef } from "react";
6
-
7
- const scaffoldStyles = cva(["flex flex-col min-h-screen w-full max-w-screen"], {
8
- variants: {},
9
- defaultVariants: {},
10
- compoundVariants: [],
11
- });
5
+ import { scaffoldClasses } from "@/components";
12
6
 
13
7
  export const Scaffold = forwardRef<HTMLDivElement, ScaffoldProps>(
14
8
  ({ className, children, ...props }, ref) => {
15
9
  const { config } = useEmperorUI();
16
10
 
17
- const backgroundColor = config?.theme?.colors?.background;
18
- const foregroundColor = config?.theme?.colors?.foreground;
19
-
20
11
  return (
21
- <div
12
+ <main
22
13
  ref={ref}
23
14
  dir={config?.interLocalization?.dir}
24
15
  data-slot="scaffold"
25
- className={cn(scaffoldStyles({ className }), className)}
26
- style={{
27
- backgroundColor: backgroundColor,
28
- color: foregroundColor,
29
- }}
16
+ className={cn(scaffoldClasses({ className }), className)}
30
17
  {...props}
31
18
  >
32
19
  {children}
33
- </div>
20
+ </main>
34
21
  );
35
22
  },
36
23
  );
@@ -0,0 +1 @@
1
+ export * from "./scaffold-classes";
@@ -0,0 +1,10 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ export const scaffoldClasses = cva(
4
+ ["flex flex-col min-h-screen w-full max-w-screen"],
5
+ {
6
+ variants: {},
7
+ defaultVariants: {},
8
+ compoundVariants: [],
9
+ },
10
+ );
@@ -1,7 +1,9 @@
1
1
  import { createPortal } from "react-dom";
2
2
  import { useEffect, useState } from "react";
3
3
  import { cn } from "@/utils";
4
- import { Button, Divider, ScrollShadow } from "@heroui/react";
4
+ import { Button } from "@heroui/button";
5
+ import { Divider } from "@heroui/divider";
6
+ import { ScrollShadow } from "@heroui/scroll-shadow";
5
7
  import { CompactSideBarProps } from "@/types";
6
8
 
7
9
  const CompactSideBarContent = ({
@@ -1,15 +1,15 @@
1
1
  import { useEmperorUI } from "@/hooks";
2
2
  import { useState } from "react";
3
3
  import {
4
- Button,
5
4
  Drawer,
6
5
  DrawerHeader,
7
6
  DrawerBody,
8
7
  DrawerContent,
9
- ScrollShadow,
10
- Divider,
11
8
  DrawerFooter,
12
- } from "@heroui/react";
9
+ } from "@heroui/drawer";
10
+ import { Button } from "@heroui/button";
11
+ import { ScrollShadow } from "@heroui/scroll-shadow";
12
+ import { Divider } from "@heroui/divider";
13
13
  import { SideBarProps } from "@/types";
14
14
  import { cn } from "@/utils";
15
15
  import { sideBarItemClasses, sideBarItemStyles } from "./styles";
@@ -29,9 +29,6 @@ export const SideBarDrawer = ({
29
29
  const { content, ...restTriggerProps } = triggerProps || {};
30
30
 
31
31
  const dir = config?.interLocalization?.dir;
32
- const backgroundColor = config?.theme?.colors?.background;
33
- const primaryColor = config?.theme?.colors?.primary;
34
- const foregroundColor = config?.theme?.colors?.foreground;
35
32
 
36
33
  const isRTL = dir === "rtl";
37
34
 
@@ -56,10 +53,7 @@ export const SideBarDrawer = ({
56
53
  dir={dir}
57
54
  className="p-0"
58
55
  >
59
- <DrawerContent
60
- className="p-0"
61
- style={{ backgroundColor, color: foregroundColor }}
62
- >
56
+ <DrawerContent className="p-0">
63
57
  <DrawerHeader className={cn("text-xl font-bold", classNames?.title)}>
64
58
  {header}
65
59
  </DrawerHeader>
@@ -76,8 +70,6 @@ export const SideBarDrawer = ({
76
70
  key={href}
77
71
  data-slot="emperor-side-bar-item"
78
72
  style={sideBarItemStyles({
79
- foregroundColor,
80
- primaryColor,
81
73
  isHovered: hoveredItem === id,
82
74
  variant,
83
75
  })}
@@ -97,10 +89,7 @@ export const SideBarDrawer = ({
97
89
 
98
90
  {actions && actions?.length > 0 && (
99
91
  <>
100
- <Divider
101
- className="m-auto w-11/12"
102
- style={{ backgroundColor: foregroundColor + "20" }}
103
- />
92
+ <Divider className="m-auto w-11/12" />
104
93
 
105
94
  <DrawerFooter className={cn(classNames?.actionsWrapper)}>
106
95
  {actions?.map(({ key, label, ...props }) => (
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
2
2
  import { Brand, SideBar } from "@/components";
3
3
  import { getStorybookDecorators } from "@/utils";
4
4
  import { MenuIcon } from "lucide-react";
5
- import { useDisclosure } from "@heroui/react";
5
+ import { useDisclosure } from "@heroui/modal";
6
6
  import { MOCK_HEADER_ACTIONS, MOCK_HEADER_ITEMS } from "@/mocks";
7
7
  import { SideBarProps } from "@/types";
8
8
 
@@ -0,0 +1,32 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Filter, Filters } from "@/components";
3
+ import { getStorybookDecorators } from "@/utils";
4
+
5
+ const meta: Meta<typeof Filters> = {
6
+ title: "Organisms/Filters",
7
+ component: Filters,
8
+ parameters: {
9
+ layout: "centered",
10
+ },
11
+ tags: ["autodocs"],
12
+ decorators: getStorybookDecorators({
13
+ config: {
14
+ layout: {
15
+ withScaffold: false,
16
+ },
17
+ },
18
+ }),
19
+ };
20
+
21
+ export default meta;
22
+
23
+ type Story = StoryObj<typeof meta>;
24
+
25
+ export const Default: Story = {
26
+ args: {},
27
+ render: () => (
28
+ <Filters>
29
+ <Filter type="search" paramKey="search" />
30
+ </Filters>
31
+ ),
32
+ };
@@ -0,0 +1,36 @@
1
+ import { cn } from "@/utils";
2
+ import { FilterProps, FiltersProps } from "@/types";
3
+ import { filtersClasses } from "./styles";
4
+ import { Children, isValidElement, ReactElement } from "react";
5
+ import { Filter } from "@/components";
6
+
7
+ export function Filters({
8
+ className,
9
+ classNames,
10
+ children,
11
+ ...props
12
+ }: FiltersProps) {
13
+ const childArray = Children.toArray(children).filter(
14
+ isValidElement,
15
+ ) as ReactElement<FilterProps, typeof Filter>[];
16
+
17
+ childArray.forEach((child) => {
18
+ const type = child.type;
19
+
20
+ if (type !== Filter) {
21
+ throw new Error(
22
+ `Allowed children for <Filters /> must be of type <Filter />. Got ${type} instead.`,
23
+ );
24
+ }
25
+ });
26
+
27
+ return (
28
+ <nav
29
+ data-slot="emperor-ui-filters"
30
+ className={cn(filtersClasses({}), className)}
31
+ {...props}
32
+ >
33
+ <ul className="flex items-center gap-2">{children}</ul>
34
+ </nav>
35
+ );
36
+ }
@@ -0,0 +1 @@
1
+ export * from "./filters";
@@ -0,0 +1,9 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ export const filtersClasses = cva([""], {
4
+ variants: {
5
+ variant: {},
6
+ },
7
+ defaultVariants: {},
8
+ compoundVariants: [],
9
+ });
@@ -0,0 +1 @@
1
+ export * from "./classes";
@@ -1,4 +1,4 @@
1
- import { cn } from "@heroui/react";
1
+ import { cn } from "@/utils";
2
2
  import { FooterProps } from "@/types";
3
3
  import { copyRightsClasses } from "./styles";
4
4
 
@@ -9,7 +9,7 @@ import {
9
9
  SocialLinksBox,
10
10
  CopyRightsBox,
11
11
  } from "@/components";
12
- import { Divider } from "@heroui/react";
12
+ import { Divider } from "@heroui/divider";
13
13
 
14
14
  export const Footer = forwardRef<
15
15
  HTMLElement,
@@ -1,4 +1,5 @@
1
- import { cn, Link } from "@heroui/react";
1
+ import { cn } from "@/utils";
2
+ import { Link } from "@heroui/link";
2
3
  import { FooterProps } from "@/types";
3
4
  import { policiesClasses } from "./styles";
4
5
 
@@ -1,4 +1,5 @@
1
- import { cn, Link } from "@heroui/react";
1
+ import { cn } from "@/utils";
2
+ import { Link } from "@heroui/link";
2
3
  import { FooterProps } from "@/types";
3
4
  import { quickLinksClasses } from "./styles";
4
5
 
@@ -1,4 +1,5 @@
1
- import { cn, Link } from "@heroui/react";
1
+ import { cn } from "@/utils";
2
+ import { Link } from "@heroui/link";
2
3
  import { FooterProps } from "@/types";
3
4
  import { socialLinksClasses } from "./styles";
4
5
 
@@ -11,7 +11,7 @@ import {
11
11
  contacts,
12
12
  socialLinks,
13
13
  } from "@/constants";
14
- import { Button } from "@heroui/react";
14
+ import { Button } from "@heroui/button";
15
15
 
16
16
  const meta: Meta<typeof Footer> = {
17
17
  title: "Organisms/Footer",
@@ -1,11 +1,10 @@
1
- import { useEmperorUI } from "@/hooks";
2
1
  import type { HeaderProps, NavBarHoverEffect, NavBarVariant } from "@/types";
3
2
  import { cn } from "@/utils";
4
3
  import { VariantProps } from "class-variance-authority";
5
4
  import { ComponentProps, forwardRef } from "react";
6
5
  import { Brand, NavBar, SideBar } from "@/components";
7
6
  import { headerClasses, headerStyles } from "./styles";
8
- import { useDisclosure } from "@heroui/react";
7
+ import { useDisclosure } from "@heroui/modal";
9
8
  import { MOCK_HEADER_ITEMS, MOCK_HEADER_ACTIONS } from "@/mocks";
10
9
  import { SegmentedHeaderContent } from "./segmented-header-content";
11
10
 
@@ -17,12 +16,8 @@ export const Header = forwardRef<
17
16
  { className, variant = "default", glassEffect, children, ...props },
18
17
  ref,
19
18
  ) => {
20
- const { config } = useEmperorUI();
21
19
  const { isOpen, onOpenChange } = useDisclosure();
22
20
 
23
- const primaryColor = config?.theme?.colors?.primary;
24
- const foregroundColor = config?.theme?.colors?.foreground;
25
-
26
21
  const content = children || (
27
22
  <>
28
23
  <SideBar
@@ -74,8 +69,6 @@ export const Header = forwardRef<
74
69
  data-slot="emperor-header"
75
70
  className={cn(headerClasses({ variant, className }))}
76
71
  style={headerStyles({
77
- primaryColor,
78
- foregroundColor,
79
72
  variant,
80
73
  glassEffect,
81
74
  })}
@@ -2,3 +2,4 @@ export * from "./item-details";
2
2
  export * from "./listings";
3
3
  export * from "./footer";
4
4
  export * from "./header";
5
+ export * from "./filters";
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import { Locale } from "@/i18n";
4
+ import { Inbox } from "lucide-react";
5
+ import { motion } from "framer-motion";
6
+ import { cn } from "@/utils";
7
+ import { emptyListingsClasses } from "@/components";
8
+ import { useEmperorUI } from "@/hooks";
9
+ import { blinkContainer, blinkItem, floating } from "@/animations";
10
+
11
+ export function EmptyListings({
12
+ className,
13
+ classNames,
14
+ }: {
15
+ className?: string;
16
+ classNames?: {
17
+ wrapper?: string;
18
+ iconWrapper?: string;
19
+ title?: string;
20
+ description?: string;
21
+ };
22
+ }) {
23
+ const { config } = useEmperorUI();
24
+
25
+ const locales = config?.interLocalization?.locales;
26
+ const lang = config?.interLocalization?.lang;
27
+
28
+ const locale = locales?.[lang || "en"] as Locale;
29
+ const listingsLocale = locale?.organisms?.listings;
30
+
31
+ return (
32
+ <motion.div
33
+ data-slot="emperor-ui-empty-listings"
34
+ className={cn(emptyListingsClasses(), className)}
35
+ variants={blinkContainer}
36
+ initial="hidden"
37
+ animate="visible"
38
+ >
39
+ <motion.div
40
+ className={cn(
41
+ "flex items-center justify-center rounded-2xl bg-default-100/80 p-6 mb-5",
42
+ classNames?.iconWrapper,
43
+ )}
44
+ variants={blinkItem}
45
+ >
46
+ <motion.div
47
+ variants={floating}
48
+ animate="animate"
49
+ className="flex items-center justify-center"
50
+ >
51
+ <Inbox
52
+ className="size-14 text-default-400"
53
+ strokeWidth={1.25}
54
+ aria-hidden
55
+ />
56
+ </motion.div>
57
+ </motion.div>
58
+
59
+ <motion.h2
60
+ className={cn(
61
+ "text-xl font-semibold text-foreground mb-2",
62
+ classNames?.title,
63
+ )}
64
+ variants={blinkItem}
65
+ >
66
+ {listingsLocale?.emptyTitle}
67
+ </motion.h2>
68
+
69
+ <motion.p
70
+ className={cn(
71
+ "text-sm text-default-500 max-w-sm",
72
+ classNames?.description,
73
+ )}
74
+ variants={blinkItem}
75
+ >
76
+ {listingsLocale?.emptyDescription}
77
+ </motion.p>
78
+ </motion.div>
79
+ );
80
+ }
@@ -1 +1,3 @@
1
1
  export * from "./listings";
2
+ export * from "./empty-listings";
3
+ export * from "./styles";
@@ -1,15 +1,96 @@
1
- import type { ListingsProps } from "@/types";
2
- import { listingsClasses, listingsStyles } from "./styles";
1
+ import { EmptyListings, ItemCard, LoadingItem } from "@/components";
2
+ import type { ItemCardOrientation, ListingsProps } from "@/types";
3
+ import { cn } from "@/utils";
4
+ import {
5
+ listingsClasses,
6
+ listingsItemClasses,
7
+ listingsPaginationClasses,
8
+ listingsStyles,
9
+ } from "./styles";
10
+ import { Pagination } from "@heroui/pagination";
11
+ import { motion } from "framer-motion";
3
12
 
4
- export function Listings<ListingType>({
13
+ export function Listings({
14
+ items = [],
15
+ isLoading = false,
5
16
  className,
6
- variant = "default",
7
- }: ListingsProps<ListingType>) {
17
+ classNames,
18
+ layout = "grid",
19
+ actions = [],
20
+ onActionClick,
21
+ pagination,
22
+ }: ListingsProps) {
23
+ const orientation: ItemCardOrientation = {
24
+ grid: "vertical",
25
+ list: "horizontal",
26
+ carousel: "vertical",
27
+ }[layout] as ItemCardOrientation;
28
+
29
+ const isEmpty = items?.length === 0;
30
+
8
31
  return (
9
- <section
32
+ <motion.section
10
33
  data-slot="emperor-ui-listings"
11
- className={listingsClasses({ className, variant })}
12
- style={listingsStyles({ variant })}
13
- ></section>
34
+ className={cn(listingsClasses({ className, layout }))}
35
+ style={listingsStyles({ layout })}
36
+ initial="hidden"
37
+ animate="visible"
38
+ >
39
+ {isLoading &&
40
+ Array.from({ length: pagination?.pageSize || 12 }).map((_, index) => (
41
+ <LoadingItem
42
+ key={index}
43
+ className={className}
44
+ classNames={classNames}
45
+ orientation={orientation}
46
+ />
47
+ ))}
48
+
49
+ {!isLoading && isEmpty && <EmptyListings className="col-span-full" />}
50
+
51
+ {!isLoading &&
52
+ !isEmpty &&
53
+ items?.map(
54
+ ({
55
+ item,
56
+ actions: itemActions,
57
+ onActionClick: onItemActionClick,
58
+ }) => (
59
+ <ItemCard
60
+ key={item?.key}
61
+ className={cn(listingsItemClasses({ layout }), classNames?.item)}
62
+ item={item}
63
+ isLoading={isLoading}
64
+ actions={itemActions || actions || []}
65
+ onActionClick={onItemActionClick || onActionClick}
66
+ orientation={orientation}
67
+ />
68
+ ),
69
+ )}
70
+
71
+ {!isLoading && !isEmpty && pagination && (
72
+ <Pagination
73
+ className={cn(
74
+ listingsPaginationClasses({ layout }),
75
+ classNames?.pagination,
76
+ )}
77
+ classNames={{
78
+ next: "rtl:rotate-180 cursor-pointer",
79
+ prev: "rtl:rotate-180 cursor-pointer",
80
+ item: "cursor-pointer",
81
+ chevronNext: "rtl:rotate-180 cursor-pointer",
82
+ forwardIcon: "rtl:rotate-180 cursor-pointer",
83
+ }}
84
+ total={pagination?.pagesCount}
85
+ page={pagination?.page}
86
+ onChange={pagination?.setPage}
87
+ variant="faded"
88
+ showControls
89
+ color="primary"
90
+ initialPage={1}
91
+ showShadow
92
+ />
93
+ )}
94
+ </motion.section>
14
95
  );
15
96
  }
@@ -0,0 +1,153 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Listings } from "@/components";
3
+ import { getStorybookDecorators } from "@/utils";
4
+ import { getListings, MOCK_LISTINGS } from "@/mocks";
5
+ import type { MockItemType } from "@/mocks";
6
+ import { useMemo, useState } from "react";
7
+ import { useEffect } from "react";
8
+ import { usePagination } from "@heroui/pagination";
9
+
10
+ const meta: Meta<typeof Listings> = {
11
+ title: "Organisms/Listings/Grid",
12
+ component: Listings,
13
+ parameters: {
14
+ layout: "centered",
15
+ },
16
+ tags: ["autodocs"],
17
+ decorators: getStorybookDecorators({
18
+ config: {
19
+ layout: {
20
+ withScaffold: false,
21
+ },
22
+ },
23
+ }),
24
+ };
25
+
26
+ export default meta;
27
+
28
+ type Story = StoryObj<typeof meta>;
29
+
30
+ export const Default: Story = {
31
+ args: {
32
+ items: MOCK_LISTINGS.map((item) => ({
33
+ item: {
34
+ key: String(item.id),
35
+ title: item?.title,
36
+ description: item.description,
37
+ image: { src: item?.image, alt: item?.title },
38
+ },
39
+ })),
40
+ },
41
+ };
42
+
43
+ export const WithLoading: Story = {
44
+ args: {},
45
+ render: () => {
46
+ const [items, setItems] = useState<MockItemType[]>([]);
47
+ const [isLoading, setIsLoading] = useState(true);
48
+
49
+ useEffect(() => {
50
+ (async () => {
51
+ const { items } = await getListings({});
52
+
53
+ setItems(items);
54
+ setIsLoading(false);
55
+ })();
56
+ }, []);
57
+
58
+ return (
59
+ <Listings
60
+ isLoading={isLoading}
61
+ items={items.map((item) => ({
62
+ item: {
63
+ key: String(item.id),
64
+ title: item?.title,
65
+ description: item.description,
66
+ image: { src: item?.image, alt: item?.title },
67
+ },
68
+ }))}
69
+ />
70
+ );
71
+ },
72
+ };
73
+
74
+ export const WithActions: Story = {
75
+ args: {
76
+ items: MOCK_LISTINGS.map((item) => ({
77
+ item: {
78
+ key: String(item.id),
79
+ title: item?.title,
80
+ description: item.description,
81
+ image: { src: item?.image, alt: item?.title },
82
+ },
83
+ actions: [
84
+ { key: "view", label: "View details" },
85
+ { key: "edit", label: "Edit" },
86
+ { key: "delete", label: "Delete" },
87
+ ],
88
+ })),
89
+ },
90
+ };
91
+
92
+ export const WithPagination: Story = {
93
+ args: {},
94
+ render: () => {
95
+ const [items, setItems] = useState<MockItemType[]>([]);
96
+ const [isLoading, setIsLoading] = useState(true);
97
+
98
+ const pageSize = 6;
99
+ const totalItemsCount = MOCK_LISTINGS.length;
100
+ const pagesCount = useMemo(
101
+ () => Math.ceil(totalItemsCount / pageSize),
102
+ [totalItemsCount, pageSize],
103
+ );
104
+
105
+ const { activePage: page, setPage } = usePagination({
106
+ total: pagesCount,
107
+ initialPage: 1,
108
+ });
109
+
110
+ useEffect(() => {
111
+ (async () => {
112
+ setIsLoading(true);
113
+
114
+ const { items } = await getListings({
115
+ page,
116
+ pageSize,
117
+ });
118
+
119
+ setItems(items);
120
+ setIsLoading(false);
121
+ })();
122
+ }, [page]);
123
+
124
+ return (
125
+ <Listings
126
+ isLoading={isLoading}
127
+ items={items.map((item) => ({
128
+ item: {
129
+ key: String(item.id),
130
+ title: item?.title,
131
+ description: item.description,
132
+ image: { src: item?.image, alt: item?.title },
133
+ banner: item?.isBestSeller ? "Best Seller" : undefined,
134
+ chips: item?.categories || [],
135
+ },
136
+ }))}
137
+ pagination={{
138
+ page,
139
+ setPage,
140
+ pageSize,
141
+ totalItemsCount,
142
+ pagesCount,
143
+ }}
144
+ />
145
+ );
146
+ },
147
+ };
148
+
149
+ export const EmptyListings: Story = {
150
+ args: {
151
+ items: [],
152
+ },
153
+ };