property-practice-ui 0.0.1

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 (189) hide show
  1. package/.prettierignore +3 -0
  2. package/.prettierrc +4 -0
  3. package/.storybook/main.ts +30 -0
  4. package/.storybook/preview.ts +32 -0
  5. package/.storybook/vitest.setup.ts +6 -0
  6. package/CHANGELOG.md +7 -0
  7. package/dist/index.js +10148 -0
  8. package/dist/index.mjs +10148 -0
  9. package/eslint.config.mjs +4 -0
  10. package/package.json +63 -0
  11. package/postcss.config.js +6 -0
  12. package/src/atoms/ArrowButton/ArrowButton.stories.tsx +52 -0
  13. package/src/atoms/ArrowButton/ArrowButton.tsx +71 -0
  14. package/src/atoms/Button/Button.stories.tsx +45 -0
  15. package/src/atoms/Button/Button.tsx +91 -0
  16. package/src/atoms/CheckboxItem/CheckboxItem.tsx +49 -0
  17. package/src/atoms/Description/Description.tsx +43 -0
  18. package/src/atoms/ExtendedButton/ExtendedButton.stories.tsx +61 -0
  19. package/src/atoms/ExtendedButton/ExtendedButton.tsx +40 -0
  20. package/src/atoms/FeatureItem/FeatureItem.stories.tsx +25 -0
  21. package/src/atoms/FeatureItem/FeatureItem.tsx +90 -0
  22. package/src/atoms/FormContainer/FormContainer.tsx +34 -0
  23. package/src/atoms/Header/Header.stories.tsx +22 -0
  24. package/src/atoms/Header/Header.tsx +53 -0
  25. package/src/atoms/Input/Input.stories.tsx +23 -0
  26. package/src/atoms/Input/Input.tsx +28 -0
  27. package/src/atoms/Label/Label.stories.tsx +28 -0
  28. package/src/atoms/Label/Label.tsx +53 -0
  29. package/src/atoms/Loader/Loader.tsx +49 -0
  30. package/src/atoms/Pill/Pill.stories.tsx +30 -0
  31. package/src/atoms/Pill/Pill.tsx +82 -0
  32. package/src/atoms/RadioItem/RadioItem.stories.tsx +25 -0
  33. package/src/atoms/RadioItem/RadioItem.tsx +54 -0
  34. package/src/atoms/SecondaryInput/SecondaryInput.stories.tsx +30 -0
  35. package/src/atoms/SecondaryInput/SecondaryInput.tsx +125 -0
  36. package/src/atoms/SocialButton/SocialButton.stories.tsx +79 -0
  37. package/src/atoms/SocialButton/SocialButton.tsx +90 -0
  38. package/src/atoms/TermsCheckbox/TermsCheckbox.stories.tsx +62 -0
  39. package/src/atoms/TermsCheckbox/TermsCheckbox.tsx +150 -0
  40. package/src/atoms/Text/Text.stories.tsx +32 -0
  41. package/src/atoms/Text/Text.tsx +49 -0
  42. package/src/atoms/TextButton/TextButton.stories.tsx +77 -0
  43. package/src/atoms/TextButton/TextButton.tsx +78 -0
  44. package/src/atoms/Textarea/Textarea.tsx +36 -0
  45. package/src/atoms/ToggleButton/ToggleButton.stories.tsx +32 -0
  46. package/src/atoms/ToggleButton/ToggleButton.tsx +106 -0
  47. package/src/atoms/index.ts +20 -0
  48. package/src/components/DynamicInput.tsx +54 -0
  49. package/src/components/FileUpload.tsx +123 -0
  50. package/src/components/Filter/Filter.tsx +33 -0
  51. package/src/components/ModeSwitch.tsx +66 -0
  52. package/src/components/NavMenu.tsx +83 -0
  53. package/src/components/SearchBar/Search.stories.tsx +25 -0
  54. package/src/components/SearchBar/Search.tsx +40 -0
  55. package/src/components/SortBy/SortBy.stories.tsx +27 -0
  56. package/src/components/SortBy/SortBy.tsx +45 -0
  57. package/src/components/Spinner.tsx +30 -0
  58. package/src/components/Table/Table.stories.tsx +56 -0
  59. package/src/components/Table/Table.tsx +25 -0
  60. package/src/components/TableInner/TableInner.tsx +27 -0
  61. package/src/components/TableInner/tableInner.stories.tsx +21 -0
  62. package/src/components/TableList.tsx +195 -0
  63. package/src/components/TableRow/TableRow.stories.tsx +55 -0
  64. package/src/components/TableRow/TableRow.tsx +343 -0
  65. package/src/components/Tabs/Tabs.stories.tsx +42 -0
  66. package/src/components/Tabs/Tabs.tsx +56 -0
  67. package/src/components/Toast.tsx +192 -0
  68. package/src/components/TopMenu.tsx +62 -0
  69. package/src/index.ts +20 -0
  70. package/src/molecules/Accordion/Accordion.stories.tsx +54 -0
  71. package/src/molecules/Accordion/Accordion.tsx +45 -0
  72. package/src/molecules/AccordionContent/AccordionContent.tsx +16 -0
  73. package/src/molecules/AccordionHeader/AccordionHeader.stories.tsx +31 -0
  74. package/src/molecules/AccordionHeader/AccordionHeader.tsx +67 -0
  75. package/src/molecules/Address/Address.stories.tsx +116 -0
  76. package/src/molecules/Address/Address.tsx +136 -0
  77. package/src/molecules/CTAContainer/CTAContainer.stories.tsx +67 -0
  78. package/src/molecules/CTAContainer/CTAContainer.tsx +98 -0
  79. package/src/molecules/Checkbox/Checkbox.tsx +60 -0
  80. package/src/molecules/ContentCard/ContentCard.stories.tsx +31 -0
  81. package/src/molecules/ContentCard/ContentCard.tsx +80 -0
  82. package/src/molecules/DocumentAccordionHeader/DocumentAccordionHeader.tsx +50 -0
  83. package/src/molecules/DocumentAccordionRow/DocumentAccordionRow.tsx +66 -0
  84. package/src/molecules/Dropdown/Dropdown.stories.tsx +39 -0
  85. package/src/molecules/Dropdown/Dropdown.tsx +89 -0
  86. package/src/molecules/EmptyState/EmptyState.stories.tsx +26 -0
  87. package/src/molecules/EmptyState/EmptyState.tsx +49 -0
  88. package/src/molecules/FAQAccordion/FAQAccordion.stories.tsx +63 -0
  89. package/src/molecules/FAQAccordion/FAQAccordion.tsx +98 -0
  90. package/src/molecules/FeatureContainer/FeatureContainer.stories.tsx +40 -0
  91. package/src/molecules/FeatureContainer/FeatureContainer.tsx +59 -0
  92. package/src/molecules/FileButton/FileButton.tsx +54 -0
  93. package/src/molecules/Input/Input.stories.tsx +30 -0
  94. package/src/molecules/Input/Input.tsx +31 -0
  95. package/src/molecules/InputContainer/InputContainer.tsx +60 -0
  96. package/src/molecules/JutaBrand/JutaBrand.stories.tsx +43 -0
  97. package/src/molecules/JutaBrand/JutaBrand.tsx +74 -0
  98. package/src/molecules/Modal/Modal.tsx +71 -0
  99. package/src/molecules/OverviewRowItem/OverviewRowItem.stories.tsx +23 -0
  100. package/src/molecules/OverviewRowItem/OverviewRowItem.tsx +39 -0
  101. package/src/molecules/PDFPreviewer/PDFPreviewer.tsx +42 -0
  102. package/src/molecules/PageLayout/PageLayout.tsx +60 -0
  103. package/src/molecules/ProductInfo/ProductInfo.stories.tsx +50 -0
  104. package/src/molecules/ProductInfo/ProductInfo.tsx +90 -0
  105. package/src/molecules/RadioGroup/RadioGroup.stories.tsx +47 -0
  106. package/src/molecules/RadioGroup/RadioGroup.tsx +55 -0
  107. package/src/molecules/RatesChart/RatesChart.stories.tsx +61 -0
  108. package/src/molecules/RatesChart/RatesChart.tsx +185 -0
  109. package/src/molecules/SideNav/SideNav.stories.tsx +64 -0
  110. package/src/molecules/SideNav/SideNav.tsx +140 -0
  111. package/src/molecules/SidePanel/SidePanel.stories.tsx +87 -0
  112. package/src/molecules/SidePanel/SidePanel.tsx +138 -0
  113. package/src/molecules/StepperHeaderTab/StepperHeaderTab.stories.tsx +23 -0
  114. package/src/molecules/StepperHeaderTab/StepperHeaderTab.tsx +43 -0
  115. package/src/molecules/Textarea/Textarea.tsx +29 -0
  116. package/src/molecules/index.ts +23 -0
  117. package/src/organism/ContactForm/ContactForm.stories.tsx +15 -0
  118. package/src/organism/ContactForm/ContactForm.tsx +54 -0
  119. package/src/organism/DocumentListAccordion/DocumentListAccordion.tsx +96 -0
  120. package/src/organism/FeatureCarousel/FeatureCarousel.stories.tsx +81 -0
  121. package/src/organism/FeatureCarousel/FeatureCarousel.tsx +162 -0
  122. package/src/organism/Footer/Footer.stories.tsx +77 -0
  123. package/src/organism/Footer/Footer.tsx +231 -0
  124. package/src/organism/Header/Header.stories.tsx +51 -0
  125. package/src/organism/Header/Header.tsx +76 -0
  126. package/src/organism/OverviewList/OverviewList.stories.tsx +43 -0
  127. package/src/organism/OverviewList/OverviewList.tsx +56 -0
  128. package/src/organism/ToastProvider/ToastProvider.tsx +40 -0
  129. package/src/organism/VersionLabel/VersionLabel.tsx +9 -0
  130. package/src/organism/index.ts +9 -0
  131. package/src/styles/tailwind.css +8 -0
  132. package/src/templates/AboutUs/AboutUs.stories.tsx +60 -0
  133. package/src/templates/AboutUs/AboutUs.tsx +97 -0
  134. package/src/templates/Contact/Contact.stories.tsx +62 -0
  135. package/src/templates/Contact/Contact.tsx +125 -0
  136. package/src/templates/FAQ/FAQ.stories.tsx +82 -0
  137. package/src/templates/FAQ/FAQ.tsx +91 -0
  138. package/src/templates/Features/Features.stories.tsx +94 -0
  139. package/src/templates/Features/Features.tsx +79 -0
  140. package/src/templates/Hero/Hero.stories.tsx +105 -0
  141. package/src/templates/Hero/Hero.tsx +139 -0
  142. package/src/templates/OtherProducts/OtherProducts.stories.tsx +77 -0
  143. package/src/templates/OtherProducts/OtherProducts.tsx +86 -0
  144. package/src/templates/index.ts +7 -0
  145. package/src/tokens/animations.ts +11 -0
  146. package/src/tokens/breakpoints.ts +7 -0
  147. package/src/tokens/colors.stories.tsx +77 -0
  148. package/src/tokens/colors.ts +59 -0
  149. package/src/tokens/radii.ts +6 -0
  150. package/src/tokens/sizes.ts +8 -0
  151. package/src/tokens/spaces.ts +21 -0
  152. package/src/types.ts +20 -0
  153. package/stories/Button.stories.ts +54 -0
  154. package/stories/Button.tsx +41 -0
  155. package/stories/Configure.mdx +364 -0
  156. package/stories/Header.stories.ts +34 -0
  157. package/stories/Header.tsx +71 -0
  158. package/stories/Page.stories.ts +33 -0
  159. package/stories/Page.tsx +91 -0
  160. package/stories/TopMenu.stories.tsx +51 -0
  161. package/stories/assets/accessibility.png +0 -0
  162. package/stories/assets/accessibility.svg +1 -0
  163. package/stories/assets/addon-library.png +0 -0
  164. package/stories/assets/assets.png +0 -0
  165. package/stories/assets/avif-test-image.avif +0 -0
  166. package/stories/assets/context.png +0 -0
  167. package/stories/assets/discord.svg +1 -0
  168. package/stories/assets/docs.png +0 -0
  169. package/stories/assets/figma-plugin.png +0 -0
  170. package/stories/assets/github.svg +1 -0
  171. package/stories/assets/share.png +0 -0
  172. package/stories/assets/styling.png +0 -0
  173. package/stories/assets/testing.png +0 -0
  174. package/stories/assets/theming.png +0 -0
  175. package/stories/assets/tutorials.svg +1 -0
  176. package/stories/assets/youtube.svg +1 -0
  177. package/stories/button.css +30 -0
  178. package/stories/header.css +32 -0
  179. package/stories/page.css +68 -0
  180. package/tailwind.config.js +34 -0
  181. package/tsconfig.json +8 -0
  182. package/types/index.ts +5 -0
  183. package/types/inputAttributes.ts +16 -0
  184. package/types/menuItem.ts +17 -0
  185. package/types/orderType.ts +2 -0
  186. package/types/tableListItem.ts +7 -0
  187. package/types/toast.ts +1 -0
  188. package/vitest.config.ts +37 -0
  189. package/vitest.shims.d.ts +1 -0
@@ -0,0 +1,106 @@
1
+ import styled from 'styled-components';
2
+ import { colors } from '../../tokens/colors';
3
+
4
+ const variants = ['primary', 'secondary', 'subtle'] as const;
5
+ type Variant = (typeof variants)[number];
6
+
7
+ const ToggleContainer = styled.div<{ variant: Variant }>`
8
+ display: inline-flex;
9
+ background-color: ${(props) => {
10
+ switch (props.variant) {
11
+ case 'primary':
12
+ return colors.background.secondary;
13
+ case 'secondary':
14
+ return colors.background.light;
15
+ case 'subtle':
16
+ return colors.background.subtle;
17
+ default:
18
+ return colors.background.secondary;
19
+ }
20
+ }};
21
+ border-radius: 9999px;
22
+ padding: 0.25rem;
23
+ gap: 0.25rem;
24
+ `;
25
+
26
+ const ToggleOption = styled.button<{
27
+ $isActive: boolean;
28
+ variant: Variant;
29
+ }>`
30
+ padding: 0.5rem 1.5rem;
31
+ border-radius: 9999px;
32
+ border: none;
33
+ font-size: 0.875rem;
34
+ font-weight: 600;
35
+ cursor: pointer;
36
+ transition: all 0.2s;
37
+ text-transform: uppercase;
38
+ letter-spacing: 0.05em;
39
+
40
+ background-color: ${props => {
41
+ if (!props.$isActive) return 'transparent';
42
+
43
+ switch (props.variant) {
44
+ case 'primary':
45
+ return colors.background.brand;
46
+ case 'secondary':
47
+ return colors.background.blue;
48
+ case 'subtle':
49
+ return colors.background.subtle;
50
+ default:
51
+ return colors.background.brand;
52
+ }
53
+ }};
54
+
55
+ color: ${props => {
56
+ if (props.$isActive) {
57
+ return colors.text.secondary;
58
+ }
59
+
60
+ switch (props.variant) {
61
+ case 'primary':
62
+ return colors.text.brand;
63
+ case 'secondary':
64
+ return colors.text.blue;
65
+ case 'subtle':
66
+ return colors.text.subtle;
67
+ default:
68
+ return colors.text.brand;
69
+ }
70
+ }};
71
+
72
+ &:hover {
73
+ opacity: ${props => props.$isActive ? 1 : 0.7};
74
+ }
75
+ `;
76
+
77
+ interface ToggleButtonProps {
78
+ options: string[];
79
+ activeOption: string;
80
+ onChange: (option: string) => void;
81
+ variant?: Variant;
82
+ }
83
+
84
+ export const ToggleButton = ({
85
+ options,
86
+ activeOption,
87
+ onChange,
88
+ variant = 'primary',
89
+ }: ToggleButtonProps) => {
90
+ return (
91
+ <ToggleContainer variant={variant}>
92
+ {options.map((option) => (
93
+ <ToggleOption
94
+ key={option}
95
+ $isActive={activeOption === option}
96
+ variant={variant}
97
+ onClick={() => onChange(option)}
98
+ >
99
+ {option}
100
+ </ToggleOption>
101
+ ))}
102
+ </ToggleContainer>
103
+ );
104
+ };
105
+
106
+ ToggleButton.variants = variants;
@@ -0,0 +1,20 @@
1
+ export { ArrowButton } from './ArrowButton/ArrowButton';
2
+ export { Button } from './Button/Button';
3
+ export { Description } from './Description/Description';
4
+ export { ExtendedButton } from './ExtendedButton/ExtendedButton';
5
+ export { FeatureItem } from './FeatureItem/FeatureItem';
6
+ export { FormContainer } from './FormContainer/FormContainer';
7
+ export { Header } from './Header/Header';
8
+ export { Input } from './Input/Input';
9
+ export { Label } from './Label/Label';
10
+ export { Loader } from './Loader/Loader';
11
+ export { Pill } from './Pill/Pill';
12
+ export { RadioItem } from './RadioItem/RadioItem';
13
+ export { SecondaryInput } from './SecondaryInput/SecondaryInput';
14
+ export { SocialButton } from './SocialButton/SocialButton';
15
+ export { TermsCheckbox } from './TermsCheckbox/TermsCheckbox';
16
+ export { Text } from './Text/Text';
17
+ export { Textarea } from './Textarea/Textarea';
18
+ export { TextButton } from './TextButton/TextButton';
19
+ export { ToggleButton } from './ToggleButton/ToggleButton';
20
+
@@ -0,0 +1,54 @@
1
+ import { InputAttributes } from '../../types/inputAttributes';
2
+
3
+ interface Props {
4
+ field: InputAttributes;
5
+ setValue: (field: string, value: string) => void;
6
+ }
7
+ export default function DynamicInput({ field, setValue }: Props) {
8
+ const control = () => {
9
+ switch (field.type) {
10
+ case 'Date':
11
+ return (
12
+ <>
13
+ <input
14
+ className="w-full border-0 text-sm bg-gray-50 text-gray-800 outline-none"
15
+ type="date"
16
+ placeholder={field.title}
17
+ required={field.required}
18
+ onChange={(event) => setValue(field.field, event.target.value)}
19
+ />
20
+ <label className="mt-auto text-xs text-gray-400 pt-2">
21
+ {field.title}
22
+ </label>
23
+ </>
24
+ );
25
+ case 'Boolean':
26
+ return (
27
+ <p className="flex flex-row">
28
+ <input
29
+ className="border-0 text-sm bg-gray-50 text-gray-800 outline-none mr-2"
30
+ type="checkbox"
31
+ placeholder={field.title}
32
+ onChange={(event) =>
33
+ setValue(field.field, event.target.checked ? 'Yes' : 'No')
34
+ }
35
+ />
36
+ <label className="mt-auto text-sm text-gray-400">
37
+ {field.title}
38
+ </label>
39
+ </p>
40
+ );
41
+ default:
42
+ return (
43
+ <input
44
+ className="w-full text-sm bg-transparent text-gray-700 dark:text-gray-300 outline-none relative py-4 px-3 rounded-lg border border-gray-400 hover:border-white focus-within:border-blue-500"
45
+ type={field.type?.toLowerCase() || 'text'}
46
+ placeholder={field.title}
47
+ required={field.required}
48
+ onChange={(event) => setValue(field.field, event.target.value)}
49
+ />
50
+ );
51
+ }
52
+ };
53
+ return <div className="flex flex-col p-2">{control()}</div>;
54
+ }
@@ -0,0 +1,123 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import axios from 'axios';
5
+
6
+ interface FileUploadFormProps {
7
+ setFile: (file: File | null) => void;
8
+ }
9
+
10
+ export default function FileUploadForm({ setFile }: FileUploadFormProps) {
11
+ const [progress, setProgress] = useState(0);
12
+ const [message, setMessage] = useState('');
13
+ const [dragOver, setDragOver] = useState(false);
14
+ const [file, setLocalFile] = useState<File | null>(null);
15
+
16
+ const handleFile = (selected: File) => {
17
+ if (selected) {
18
+ setLocalFile(selected);
19
+ setFile(selected);
20
+ }
21
+ };
22
+
23
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
24
+ const selected = e.target.files?.[0];
25
+ if (selected) handleFile(selected);
26
+ };
27
+
28
+ const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
29
+ event.preventDefault();
30
+ setDragOver(false);
31
+ const droppedFile = event.dataTransfer.files?.[0];
32
+ if (droppedFile) handleFile(droppedFile);
33
+ };
34
+
35
+ const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
36
+ event.preventDefault();
37
+ setDragOver(true);
38
+ };
39
+
40
+ const handleDragLeave = () => setDragOver(false);
41
+
42
+ const toBase64 = (file: File): Promise<string> =>
43
+ new Promise((resolve, reject) => {
44
+ const reader = new FileReader();
45
+ reader.readAsDataURL(file);
46
+ reader.onload = () => resolve((reader.result as string).split(',')[1]);
47
+ reader.onerror = reject;
48
+ });
49
+
50
+ const handleSubmit = async (e: React.FormEvent) => {
51
+ e.preventDefault();
52
+ setProgress(0);
53
+ setMessage('');
54
+ if (!file) return setMessage('❌ No file selected.');
55
+
56
+ try {
57
+ const base64 = await toBase64(file);
58
+
59
+ await axios.post(
60
+ '/api/upload',
61
+ {
62
+ fileName: file.name,
63
+ base64File: base64,
64
+ },
65
+ {
66
+ onUploadProgress: (event) => {
67
+ const percent = event.total
68
+ ? Math.round((event.loaded / event.total) * 100)
69
+ : 0;
70
+ setProgress(percent);
71
+ },
72
+ },
73
+ );
74
+
75
+ setMessage('✅ Submitted successfully!');
76
+ } catch (err) {
77
+ console.error(err);
78
+ setMessage('❌ Upload failed.');
79
+ }
80
+ };
81
+
82
+ return (
83
+ <form
84
+ onSubmit={handleSubmit}
85
+ className="max-w-xl mx-auto p-6 space-y-6 bg-white dark:bg-transparent rounded-lg shadow"
86
+ >
87
+ {/* Drag and Drop + Button + Preview */}
88
+ <div
89
+ onDragOver={handleDragOver}
90
+ onDragLeave={handleDragLeave}
91
+ onDrop={handleDrop}
92
+ className={`flex flex-col items-center justify-center px-4 py-10 border-gray-300 border-2 border-dashed rounded-md text-center transition ${
93
+ dragOver ? 'bg-blue-50' : 'bg-white dark:bg-transparent '
94
+ }`}
95
+ >
96
+ +
97
+ <p className="text-sm text-gray-300 mb-2">
98
+ {file ? `Selected: ${file.name}` : 'Drag & drop your file here'}
99
+ </p>
100
+ </div>
101
+
102
+ {/* Progress Bar */}
103
+ {progress > 0 && (
104
+ <div className="w-full bg-gray-200 rounded-full h-4 overflow-hidden">
105
+ <div
106
+ className="bg-blue-600 h-4 transition-all"
107
+ style={{ width: `${progress}%` }}
108
+ />
109
+ </div>
110
+ )}
111
+
112
+ {message && (
113
+ <p
114
+ className={`text-sm ${
115
+ message.includes('✅') ? 'text-green-600' : 'text-red-600'
116
+ }`}
117
+ >
118
+ {message}
119
+ </p>
120
+ )}
121
+ </form>
122
+ );
123
+ }
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+
3
+ import { Dropdown } from '@repo/property-practice-ui';
4
+ import { Option } from '../../types';
5
+
6
+ interface FilterProps<T extends string | number> {
7
+ name: string;
8
+ label?: string;
9
+ options: Option<T>[];
10
+ value: T;
11
+ onChange: (value: T) => void;
12
+ placeholder?: string;
13
+ }
14
+
15
+ export function Filter<T extends string | number>({
16
+ name,
17
+ label,
18
+ options,
19
+ value,
20
+ onChange,
21
+ placeholder = 'Filter by User',
22
+ }: FilterProps<T>) {
23
+ return (
24
+ <Dropdown
25
+ name={name}
26
+ label={label}
27
+ options={options}
28
+ value={value}
29
+ onChange={onChange}
30
+ placeholder={placeholder}
31
+ />
32
+ );
33
+ }
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+
3
+ const toggleMode = (mode: string) => {
4
+ document.cookie = `mode=${mode}; path=/; max-age=31536000`;
5
+ window.location.reload();
6
+ };
7
+
8
+ export default function SwitchButton() {
9
+ return (
10
+ <div className="inline-flex shadow-md bg-white dark:bg-gray-500 shadow-gray-900 rounded-xl border-0">
11
+ <button
12
+ onClick={() => toggleMode('')}
13
+ className="p-1 border-r border-1 border-gray-800"
14
+ >
15
+ <svg
16
+ fill="#d0d0d0"
17
+ viewBox="0 0 24 24"
18
+ height="20px"
19
+ width="20px"
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ >
22
+ <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
23
+ <g
24
+ id="SVGRepo_tracerCarrier"
25
+ strokeLinecap="round"
26
+ strokeLinejoin="round"
27
+ ></g>
28
+ <g id="SVGRepo_iconCarrier">
29
+ <path
30
+ d="M12 3V4M12 20V21M4 12H3M6.31412 6.31412L5.5 5.5M17.6859 6.31412L18.5 5.5M6.31412 17.69L5.5 18.5001M17.6859 17.69L18.5 18.5001M21 12H20M16 12C16 14.2091 14.2091 16 12 16C9.79086 16 8 14.2091 8 12C8 9.79086 9.79086 8 12 8C14.2091 8 16 9.79086 16 12Z"
31
+ stroke="#d0d0d0"
32
+ strokeWidth="2"
33
+ strokeLinecap="round"
34
+ strokeLinejoin="round"
35
+ ></path>
36
+ </g>
37
+ </svg>
38
+ </button>
39
+ <button onClick={() => toggleMode('dark')} className="p-1.5">
40
+ <svg
41
+ fill="#757575"
42
+ height="16px"
43
+ width="16px"
44
+ version="1.1"
45
+ id="Layer_1"
46
+ xmlns="http://www.w3.org/2000/svg"
47
+ viewBox="0 0 472.618 472.618"
48
+ >
49
+ <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
50
+ <g
51
+ id="SVGRepo_tracerCarrier"
52
+ strokeLinecap="round"
53
+ strokeLinejoin="round"
54
+ ></g>
55
+ <g id="SVGRepo_iconCarrier">
56
+ <g>
57
+ <g>
58
+ <path d="M380.525,337.291c-135.427,0-245.302-109.773-245.302-245.302c0-32.502,6.338-63.575,17.991-91.988 C63.372,36.286,0,124.39,0,227.315c0,135.427,109.875,245.302,245.302,245.302c102.923,0,191.029-63.472,227.316-153.315 C444.201,330.954,413.129,337.291,380.525,337.291z"></path>
59
+ </g>
60
+ </g>
61
+ </g>
62
+ </svg>
63
+ </button>
64
+ </div>
65
+ );
66
+ }
@@ -0,0 +1,83 @@
1
+ import { MenuItem, MenuSubItem } from '../../types/menuItem';
2
+ import Link from 'next/link';
3
+ import { tv } from 'tailwind-variants';
4
+
5
+ interface Props {
6
+ direction: 'horizontal' | 'vertical';
7
+ items: MenuItem[];
8
+ selected?: string;
9
+ }
10
+
11
+ export default function NavMenu({ direction, items, selected = '' }: Props) {
12
+ const containerStyle = tv({
13
+ base: 'flex',
14
+ variants: {
15
+ direction: {
16
+ vertical: 'flex-col',
17
+ horizontal: 'flex-row',
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ direction: 'horizontal',
22
+ },
23
+ });
24
+
25
+ return (
26
+ <ul className={containerStyle({ direction })}>
27
+ {items.map((item: MenuItem, i: number) => (
28
+ <li
29
+ key={i}
30
+ className="mb-2 max-h-10 hover:max-h-400 transition-all overflow-hidden duration-500"
31
+ >
32
+ <Link
33
+ href={item.link || ''}
34
+ className={` ${item.link == selected ? 'font-bold text-gray-100' : 'font-semibold text-gray-200'} flex mb-2 py-3 px-4 items-center justify-between text-sm rounded-2xl hover:underline hover:font-bold transition duration-200`}
35
+ >
36
+ <div className="flex items-center gap-2">
37
+ {item.icon && item.icon}
38
+ <span>{item.title}</span>
39
+ </div>
40
+ {item.items && (
41
+ <span className="transform">
42
+ <svg
43
+ xmlns="http://www.w3.org/2000/svg"
44
+ width={20}
45
+ height={20}
46
+ viewBox="0 0 24 24"
47
+ fill="none"
48
+ >
49
+ <path
50
+ d="M15.1746 10.1204C15.3843 9.94067 15.6999 9.96495 15.8796 10.1746C16.0593 10.3843 16.0351 10.6999 15.8254 10.8796L12.3254 13.8796C12.1382 14.0401 11.8619 14.0401 11.6746 13.8796L8.17461 10.8796C7.96495 10.6999 7.94067 10.3843 8.12038 10.1746C8.30009 9.96495 8.61574 9.94067 8.8254 10.1204L12 12.8415L15.1746 10.1204Z"
51
+ fill="currentColor"
52
+ />
53
+ </svg>
54
+ </span>
55
+ )}
56
+ </Link>
57
+ <div>
58
+ {item.items && (
59
+ <ul className="pl-8 p-4 rounded-md">
60
+ {item.items.map((subItem: MenuSubItem, i: number) => (
61
+ <li key={i} className="mb-2">
62
+ <Link
63
+ href={subItem.link || '#'}
64
+ className="flex mb-2 py-3 px-4 items-center text-gray-300 font-semibold text-xs rounded-2xl hover:underline hover:font-bold hover:text-gray-800 transition duration-200"
65
+ >
66
+ {subItem.icon && (
67
+ <span
68
+ className="mr-2"
69
+ dangerouslySetInnerHTML={{ __html: subItem.icon }}
70
+ ></span>
71
+ )}
72
+ <span>{subItem.title}</span>
73
+ </Link>
74
+ </li>
75
+ ))}
76
+ </ul>
77
+ )}
78
+ </div>
79
+ </li>
80
+ ))}
81
+ </ul>
82
+ );
83
+ }
@@ -0,0 +1,25 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { Search as Component } from '../SearchBar/Search';
3
+
4
+ const meta: Meta = {
5
+ title: 'Components/Search',
6
+ component: Component,
7
+ };
8
+
9
+ export default meta;
10
+
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const Default: Story = {
14
+ args: {
15
+ value: '',
16
+ placeholder: 'Search...',
17
+ delay: 1000,
18
+ onChange: (val: string) => console.log('Search term changed:', val),
19
+ },
20
+ argTypes: {
21
+ value: { control: 'text' },
22
+ placeholder: { control: 'text' },
23
+ delay: { control: 'number' },
24
+ },
25
+ };
@@ -0,0 +1,40 @@
1
+ import { ChangeEvent, useEffect, useState } from 'react';
2
+
3
+ interface SearchProps {
4
+ value: string;
5
+ onChange: (value: string) => void;
6
+ placeholder?: string;
7
+ delay?: number;
8
+ }
9
+
10
+ export const Search = ({
11
+ value,
12
+ onChange,
13
+ placeholder = 'Search...',
14
+ delay = 1000,
15
+ }: SearchProps) => {
16
+ const [searchTerm, setSearchTerm] = useState(value);
17
+
18
+ useEffect(() => {
19
+ const handler = setTimeout(() => {
20
+ onChange(searchTerm);
21
+ }, delay);
22
+
23
+ return () => clearTimeout(handler);
24
+ }, [searchTerm, delay, onChange]);
25
+
26
+ const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
27
+ setSearchTerm(e.target.value);
28
+ };
29
+
30
+ return (
31
+ <input
32
+ type="text"
33
+ value={searchTerm}
34
+ onChange={handleChange}
35
+ placeholder={placeholder}
36
+ className="border border-gray-300 rounded-md px-4 py-2 w-64 text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
37
+ aria-label={placeholder}
38
+ />
39
+ );
40
+ };
@@ -0,0 +1,27 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { OrderType } from '../../../types/orderType';
3
+ import { SortBy as Component } from './SortBy';
4
+
5
+ const meta: Meta = {
6
+ title: 'Components/SortBy',
7
+ component: Component,
8
+ };
9
+
10
+ export default meta;
11
+
12
+ type Story = StoryObj<typeof meta>;
13
+
14
+ export const Default: Story = {
15
+ args: {
16
+ value: 'desc',
17
+ onChange: (val: OrderType) => console.log('Changed sort to:', val),
18
+ },
19
+ argTypes: {
20
+ value: {
21
+ control: {
22
+ type: 'select',
23
+ options: ['asc', 'desc'],
24
+ },
25
+ },
26
+ },
27
+ };
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { OrderType } from '../../../types/orderType';
5
+
6
+ interface SortByProps {
7
+ value: OrderType;
8
+ onChange: (value: OrderType) => void;
9
+ }
10
+
11
+ const Options: { value: OrderType; label: string }[] = [
12
+ {
13
+ value: 'desc',
14
+ label: 'Sort by: Newest',
15
+ },
16
+ {
17
+ value: 'asc',
18
+ label: 'Sort by: Oldest',
19
+ },
20
+ ];
21
+
22
+ export const SortBy = ({ value, onChange }: SortByProps) => {
23
+ const [selected, setSelected] = useState<OrderType>(value);
24
+
25
+ useEffect(() => {
26
+ const handler = setTimeout(() => {
27
+ onChange(selected);
28
+ });
29
+ return () => clearTimeout(handler);
30
+ }, [selected, onChange]);
31
+
32
+ return (
33
+ <select
34
+ value={selected}
35
+ onChange={(e) => setSelected(e.target.value as OrderType)}
36
+ className="border border-gray-300 rounded-md px-4 py-2 w-48 text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
37
+ >
38
+ {Options.map((option) => (
39
+ <option key={option.value} value={option.value}>
40
+ {option.label}
41
+ </option>
42
+ ))}
43
+ </select>
44
+ );
45
+ };
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+
3
+ interface SpinnerProps {
4
+ height?: `${number}px`;
5
+ width?: `${number}px`;
6
+ }
7
+
8
+ export default function Spinner(props: SpinnerProps) {
9
+ return (
10
+ <>
11
+ <svg
12
+ aria-hidden="true"
13
+ className="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600"
14
+ viewBox="0 0 100 101"
15
+ fill="none"
16
+ xmlns="http://www.w3.org/2000/svg"
17
+ {...props}
18
+ >
19
+ <path
20
+ d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
21
+ fill="currentColor"
22
+ />
23
+ <path
24
+ d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
25
+ fill="currentFill"
26
+ />
27
+ </svg>
28
+ </>
29
+ );
30
+ }