@umituz/web-design-system 3.1.21 → 4.0.0
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/README.md +137 -469
- package/dist/styles.css +2 -0
- package/package.json +30 -31
- package/src/__tests__/cn.test.ts +24 -0
- package/src/__tests__/components.smoke.test.tsx +78 -0
- package/src/__tests__/styles.contract.test.ts +60 -0
- package/src/domain/tokens/index.ts +8 -49
- package/src/index.ts +8 -10
- package/src/infrastructure/constants/component.constants.ts +3 -17
- package/src/infrastructure/constants/index.ts +4 -2
- package/src/infrastructure/constants/timing.constants.ts +46 -0
- package/src/infrastructure/utils/calculation.utils.ts +74 -0
- package/src/infrastructure/utils/index.ts +12 -0
- package/src/presentation/atoms/IconButton.tsx +55 -0
- package/src/presentation/atoms/Progress.tsx +2 -1
- package/src/presentation/atoms/Spinner.tsx +1 -1
- package/src/presentation/atoms/Switch.tsx +2 -2
- package/src/presentation/atoms/Text.tsx +15 -13
- package/src/presentation/atoms/Tooltip.tsx +2 -1
- package/src/presentation/atoms/index.ts +3 -0
- package/src/presentation/hooks/index.ts +0 -3
- package/src/presentation/hooks/useClickOutside.ts +14 -10
- package/src/presentation/hooks/useClipboard.ts +30 -12
- package/src/presentation/hooks/useDebounce.ts +8 -10
- package/src/presentation/hooks/useLocalStorage.ts +28 -18
- package/src/presentation/hooks/useMediaQuery.ts +2 -1
- package/src/presentation/hooks/useScrollLock.ts +40 -12
- package/src/presentation/hooks/useToggle.ts +8 -6
- package/src/presentation/molecules/ActiveFilterTags.tsx +19 -18
- package/src/presentation/molecules/Avatar.tsx +1 -1
- package/src/presentation/molecules/CodeBlock.tsx +51 -48
- package/src/presentation/molecules/ListItem.tsx +40 -29
- package/src/presentation/molecules/Select.tsx +1 -1
- package/src/presentation/molecules/Toggle.tsx +4 -4
- package/src/presentation/molecules/index.ts +0 -3
- package/src/presentation/organisms/Alert.tsx +1 -1
- package/src/presentation/organisms/DataTable.tsx +24 -11
- package/src/presentation/organisms/Dialog.tsx +1 -1
- package/src/presentation/organisms/DropdownMenu.tsx +2 -2
- package/src/presentation/organisms/HoverCard.tsx +1 -1
- package/src/presentation/organisms/LoadingState.tsx +1 -5
- package/src/presentation/organisms/Popover.tsx +1 -1
- package/src/presentation/organisms/StatCard.tsx +2 -2
- package/src/presentation/organisms/Table.tsx +2 -2
- package/src/presentation/organisms/index.ts +0 -60
- package/src/styles/compile.css +18 -0
- package/src/styles/theme.css +131 -0
- package/src/domain/tokens/animation.tokens.ts +0 -87
- package/src/domain/tokens/color.tokens.ts +0 -95
- package/src/domain/tokens/radius.tokens.ts +0 -27
- package/src/domain/tokens/shadow.tokens.ts +0 -25
- package/src/domain/tokens/spacing.tokens.ts +0 -65
- package/src/domain/tokens/typography.tokens.ts +0 -74
- package/src/infrastructure/error/ErrorBoundary.tsx +0 -258
- package/src/infrastructure/error/ErrorDisplay.tsx +0 -346
- package/src/infrastructure/error/SuspenseWrapper.tsx +0 -133
- package/src/infrastructure/error/index.ts +0 -5
- package/src/infrastructure/performance/index.ts +0 -92
- package/src/infrastructure/performance/useLazyLoading.ts +0 -342
- package/src/infrastructure/performance/useMemoryOptimization.ts +0 -293
- package/src/infrastructure/performance/usePerformanceMonitor.ts +0 -158
- package/src/infrastructure/security/index.ts +0 -10
- package/src/infrastructure/security/security-config.ts +0 -171
- package/src/infrastructure/security/useFormValidation.ts +0 -216
- package/src/infrastructure/security/validation.ts +0 -246
- package/src/presentation/hooks/useLanguage.ts +0 -109
- package/src/presentation/molecules/Breadcrumb.tsx +0 -54
- package/src/presentation/organisms/AlertDialog.tsx +0 -181
- package/src/presentation/organisms/Calendar.tsx +0 -79
- package/src/presentation/organisms/Comments.tsx +0 -103
- package/src/presentation/organisms/ConfirmDialog.tsx +0 -97
- package/src/presentation/organisms/FilterBar.tsx +0 -244
- package/src/presentation/organisms/FilterSidebar.tsx +0 -172
- package/src/presentation/organisms/Footer.tsx +0 -86
- package/src/presentation/organisms/FormModal.tsx +0 -126
- package/src/presentation/organisms/Grid.tsx +0 -128
- package/src/presentation/organisms/ImageLightbox.tsx +0 -252
- package/src/presentation/organisms/MainNavbar.tsx +0 -320
- package/src/presentation/organisms/Modal.tsx +0 -173
- package/src/presentation/organisms/Navbar.tsx +0 -75
- package/src/presentation/organisms/NewsletterSignup.tsx +0 -165
- package/src/presentation/organisms/QuickActionCard.tsx +0 -93
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FilterBar Component (Organism)
|
|
3
|
-
* @description Mobile filter bar with search, categories, tags, and sort with optimized performance
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useState, useCallback, memo } from 'react';
|
|
7
|
-
import React from 'react';
|
|
8
|
-
import type { BaseProps } from '../../domain/types';
|
|
9
|
-
|
|
10
|
-
export interface Category {
|
|
11
|
-
id: string;
|
|
12
|
-
name: string;
|
|
13
|
-
count: number;
|
|
14
|
-
icon: React.ComponentType<{ className?: string; size?: number }>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface SortOption {
|
|
18
|
-
id: string;
|
|
19
|
-
name: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface FilterBarProps extends BaseProps {
|
|
23
|
-
searchQuery: string;
|
|
24
|
-
setSearchQuery: (query: string) => void;
|
|
25
|
-
selectedCategory: string | null;
|
|
26
|
-
onCategoryChange: (category: string) => void;
|
|
27
|
-
categories: Category[];
|
|
28
|
-
popularTags: string[];
|
|
29
|
-
selectedTags: string[];
|
|
30
|
-
onTagToggle: (tag: string) => void;
|
|
31
|
-
sortBy: string;
|
|
32
|
-
onSortChange: (sort: string) => void;
|
|
33
|
-
sortOptions: readonly SortOption[] | SortOption[];
|
|
34
|
-
hasActiveFilters: boolean;
|
|
35
|
-
onClearFilters: () => void;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Memoize category button component
|
|
39
|
-
const CategoryButton = memo<{
|
|
40
|
-
category: Category;
|
|
41
|
-
selectedCategory: string | null;
|
|
42
|
-
onCategoryChange: (category: string) => void;
|
|
43
|
-
}>(({ category, selectedCategory, onCategoryChange }) => {
|
|
44
|
-
const Icon = category.icon;
|
|
45
|
-
const isActive = selectedCategory === category.id;
|
|
46
|
-
|
|
47
|
-
const handleClick = useCallback(() => {
|
|
48
|
-
onCategoryChange(category.id);
|
|
49
|
-
}, [category.id, onCategoryChange]);
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<button
|
|
53
|
-
onClick={handleClick}
|
|
54
|
-
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-xs transition-all transition-theme ${
|
|
55
|
-
isActive
|
|
56
|
-
? 'bg-primary-light text-text-primary font-medium'
|
|
57
|
-
: 'bg-bg-secondary text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
|
|
58
|
-
}`}
|
|
59
|
-
type="button"
|
|
60
|
-
aria-pressed={isActive}
|
|
61
|
-
>
|
|
62
|
-
<Icon size={14} />
|
|
63
|
-
<span>{category.name}</span>
|
|
64
|
-
</button>
|
|
65
|
-
);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
CategoryButton.displayName = 'CategoryButton';
|
|
69
|
-
|
|
70
|
-
// Memoize tag button component
|
|
71
|
-
const TagButton = memo<{
|
|
72
|
-
tag: string;
|
|
73
|
-
isSelected: boolean;
|
|
74
|
-
onTagToggle: (tag: string) => void;
|
|
75
|
-
}>(({ tag, isSelected, onTagToggle }) => {
|
|
76
|
-
const handleClick = useCallback(() => {
|
|
77
|
-
onTagToggle(tag);
|
|
78
|
-
}, [tag, onTagToggle]);
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<button
|
|
82
|
-
onClick={handleClick}
|
|
83
|
-
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-all transition-theme ${
|
|
84
|
-
isSelected
|
|
85
|
-
? 'bg-primary-light text-text-primary'
|
|
86
|
-
: 'bg-bg-secondary text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
|
|
87
|
-
}`}
|
|
88
|
-
type="button"
|
|
89
|
-
aria-pressed={isSelected}
|
|
90
|
-
>
|
|
91
|
-
{tag}
|
|
92
|
-
</button>
|
|
93
|
-
);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
TagButton.displayName = 'TagButton';
|
|
97
|
-
|
|
98
|
-
export const FilterBar = memo<FilterBarProps>(({
|
|
99
|
-
searchQuery,
|
|
100
|
-
setSearchQuery,
|
|
101
|
-
selectedCategory,
|
|
102
|
-
onCategoryChange,
|
|
103
|
-
categories,
|
|
104
|
-
popularTags,
|
|
105
|
-
selectedTags,
|
|
106
|
-
onTagToggle,
|
|
107
|
-
sortBy,
|
|
108
|
-
onSortChange,
|
|
109
|
-
sortOptions,
|
|
110
|
-
hasActiveFilters,
|
|
111
|
-
onClearFilters,
|
|
112
|
-
className,
|
|
113
|
-
}) => {
|
|
114
|
-
const [isFilterOpen, setIsFilterOpen] = useState(false);
|
|
115
|
-
|
|
116
|
-
const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
117
|
-
setSearchQuery(e.target.value);
|
|
118
|
-
}, [setSearchQuery]);
|
|
119
|
-
|
|
120
|
-
const handleToggleFilters = useCallback(() => {
|
|
121
|
-
setIsFilterOpen(prev => !prev);
|
|
122
|
-
}, []);
|
|
123
|
-
|
|
124
|
-
const handleSortChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
125
|
-
onSortChange(e.target.value);
|
|
126
|
-
}, [onSortChange]);
|
|
127
|
-
|
|
128
|
-
const handleClearFilters = useCallback(() => {
|
|
129
|
-
onClearFilters();
|
|
130
|
-
}, [onClearFilters]);
|
|
131
|
-
|
|
132
|
-
return (
|
|
133
|
-
<div className={`lg:hidden mb-4 space-y-3 ${className || ''}`}>
|
|
134
|
-
{/* Search */}
|
|
135
|
-
<div className="relative">
|
|
136
|
-
<svg
|
|
137
|
-
className="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary"
|
|
138
|
-
width="18"
|
|
139
|
-
height="18"
|
|
140
|
-
viewBox="0 0 24 24"
|
|
141
|
-
fill="none"
|
|
142
|
-
stroke="currentColor"
|
|
143
|
-
strokeWidth="2"
|
|
144
|
-
strokeLinecap="round"
|
|
145
|
-
strokeLinejoin="round"
|
|
146
|
-
>
|
|
147
|
-
<circle cx="11" cy="11" r="8" />
|
|
148
|
-
<path d="m21 21-4.35-4.35" />
|
|
149
|
-
</svg>
|
|
150
|
-
<input
|
|
151
|
-
type="text"
|
|
152
|
-
placeholder="Search..."
|
|
153
|
-
value={searchQuery}
|
|
154
|
-
onChange={handleSearchChange}
|
|
155
|
-
className="w-full pl-9 pr-4 py-2.5 bg-bg-secondary text-text-primary rounded-lg border border-border focus:border-primary-light focus:outline-none placeholder-text-secondary/50 transition-theme text-sm"
|
|
156
|
-
aria-label="Search"
|
|
157
|
-
/>
|
|
158
|
-
</div>
|
|
159
|
-
|
|
160
|
-
{/* Filter Toggle + Sort + Clear Filters */}
|
|
161
|
-
<div className="flex items-center gap-2 flex-wrap">
|
|
162
|
-
<button
|
|
163
|
-
onClick={handleToggleFilters}
|
|
164
|
-
className="flex items-center gap-2 px-4 py-2.5 bg-bg-secondary text-text-primary rounded-lg border border-border hover:border-primary-light transition-all text-sm font-medium transition-theme"
|
|
165
|
-
type="button"
|
|
166
|
-
aria-expanded={isFilterOpen}
|
|
167
|
-
aria-label="Toggle filters"
|
|
168
|
-
>
|
|
169
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
170
|
-
<path d="M12 20v-6M6 20V10M18 20V4" />
|
|
171
|
-
</svg>
|
|
172
|
-
Filters
|
|
173
|
-
{(selectedCategory || selectedTags.length > 0) && (
|
|
174
|
-
<span className="ml-1 px-2 py-0.5 bg-primary-light text-text-primary text-xs rounded-full">
|
|
175
|
-
{[selectedCategory, ...selectedTags].filter(Boolean).length}
|
|
176
|
-
</span>
|
|
177
|
-
)}
|
|
178
|
-
</button>
|
|
179
|
-
|
|
180
|
-
{/* Sort Dropdown */}
|
|
181
|
-
<select
|
|
182
|
-
value={sortBy}
|
|
183
|
-
onChange={handleSortChange}
|
|
184
|
-
className="px-3 py-2.5 bg-bg-secondary text-text-primary rounded-lg border border-border focus:border-primary-light focus:outline-none transition-theme text-sm"
|
|
185
|
-
aria-label="Sort by"
|
|
186
|
-
>
|
|
187
|
-
{sortOptions.map((option) => (
|
|
188
|
-
<option key={option.id} value={option.id}>
|
|
189
|
-
{option.name}
|
|
190
|
-
</option>
|
|
191
|
-
))}
|
|
192
|
-
</select>
|
|
193
|
-
|
|
194
|
-
{/* Clear Filters */}
|
|
195
|
-
{hasActiveFilters && (
|
|
196
|
-
<button
|
|
197
|
-
onClick={handleClearFilters}
|
|
198
|
-
className="px-3 py-2.5 text-text-secondary hover:text-text-primary text-sm transition-colors"
|
|
199
|
-
type="button"
|
|
200
|
-
>
|
|
201
|
-
Clear All
|
|
202
|
-
</button>
|
|
203
|
-
)}
|
|
204
|
-
</div>
|
|
205
|
-
|
|
206
|
-
{/* Mobile Filter Drawer */}
|
|
207
|
-
{isFilterOpen && (
|
|
208
|
-
<div className="bg-bg-card rounded-xl p-4 border border-border space-y-4 transition-theme">
|
|
209
|
-
{/* Categories */}
|
|
210
|
-
<div>
|
|
211
|
-
<h4 className="text-sm font-semibold text-text-primary mb-3">Categories</h4>
|
|
212
|
-
<div className="grid grid-cols-2 gap-2">
|
|
213
|
-
{categories.map((category) => (
|
|
214
|
-
<CategoryButton
|
|
215
|
-
key={category.id}
|
|
216
|
-
category={category}
|
|
217
|
-
selectedCategory={selectedCategory}
|
|
218
|
-
onCategoryChange={onCategoryChange}
|
|
219
|
-
/>
|
|
220
|
-
))}
|
|
221
|
-
</div>
|
|
222
|
-
</div>
|
|
223
|
-
|
|
224
|
-
{/* Popular Tags */}
|
|
225
|
-
<div>
|
|
226
|
-
<h4 className="text-sm font-semibold text-text-primary mb-3">Popular Tags</h4>
|
|
227
|
-
<div className="flex flex-wrap gap-2">
|
|
228
|
-
{popularTags.map((tag) => (
|
|
229
|
-
<TagButton
|
|
230
|
-
key={tag}
|
|
231
|
-
tag={tag}
|
|
232
|
-
isSelected={selectedTags.includes(tag)}
|
|
233
|
-
onTagToggle={onTagToggle}
|
|
234
|
-
/>
|
|
235
|
-
))}
|
|
236
|
-
</div>
|
|
237
|
-
</div>
|
|
238
|
-
</div>
|
|
239
|
-
)}
|
|
240
|
-
</div>
|
|
241
|
-
);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
FilterBar.displayName = 'FilterBar';
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FilterSidebar Component (Organism)
|
|
3
|
-
* @description Desktop sidebar with search, categories, tags, and sort options
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React from 'react';
|
|
7
|
-
import type { BaseProps } from '../../domain/types';
|
|
8
|
-
|
|
9
|
-
export interface Category {
|
|
10
|
-
id: string;
|
|
11
|
-
name: string;
|
|
12
|
-
count: number;
|
|
13
|
-
icon: React.ComponentType<{ className?: string; size?: number }>;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface SortOption {
|
|
17
|
-
id: string;
|
|
18
|
-
name: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface FilterSidebarProps extends BaseProps {
|
|
22
|
-
searchQuery: string;
|
|
23
|
-
setSearchQuery: (query: string) => void;
|
|
24
|
-
selectedCategory: string | null;
|
|
25
|
-
onCategoryChange: (category: string) => void;
|
|
26
|
-
categories: Category[];
|
|
27
|
-
popularTags: string[];
|
|
28
|
-
selectedTags: string[];
|
|
29
|
-
onTagToggle: (tag: string) => void;
|
|
30
|
-
sortBy: string;
|
|
31
|
-
onSortChange: (sort: string) => void;
|
|
32
|
-
sortOptions: readonly SortOption[] | SortOption[];
|
|
33
|
-
hasActiveFilters: boolean;
|
|
34
|
-
onClearFilters: () => void;
|
|
35
|
-
children?: React.ReactNode;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const FilterSidebar = ({
|
|
39
|
-
searchQuery,
|
|
40
|
-
setSearchQuery,
|
|
41
|
-
selectedCategory,
|
|
42
|
-
onCategoryChange,
|
|
43
|
-
categories,
|
|
44
|
-
popularTags,
|
|
45
|
-
selectedTags,
|
|
46
|
-
onTagToggle,
|
|
47
|
-
sortBy,
|
|
48
|
-
onSortChange,
|
|
49
|
-
sortOptions,
|
|
50
|
-
hasActiveFilters,
|
|
51
|
-
onClearFilters,
|
|
52
|
-
children,
|
|
53
|
-
className,
|
|
54
|
-
}: FilterSidebarProps) => {
|
|
55
|
-
return (
|
|
56
|
-
<aside className={`hidden lg:block w-64 flex-shrink-0 ${className || ''}`}>
|
|
57
|
-
<div className="sticky top-24 space-y-6">
|
|
58
|
-
{/* Search */}
|
|
59
|
-
<div className="relative">
|
|
60
|
-
<svg
|
|
61
|
-
className="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary"
|
|
62
|
-
width="18"
|
|
63
|
-
height="18"
|
|
64
|
-
viewBox="0 0 24 24"
|
|
65
|
-
fill="none"
|
|
66
|
-
stroke="currentColor"
|
|
67
|
-
strokeWidth="2"
|
|
68
|
-
strokeLinecap="round"
|
|
69
|
-
strokeLinejoin="round"
|
|
70
|
-
>
|
|
71
|
-
<circle cx="11" cy="11" r="8" />
|
|
72
|
-
<path d="m21 21-4.35-4.35" />
|
|
73
|
-
</svg>
|
|
74
|
-
<input
|
|
75
|
-
type="text"
|
|
76
|
-
placeholder="Search..."
|
|
77
|
-
value={searchQuery}
|
|
78
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
79
|
-
className="w-full pl-9 pr-4 py-2.5 bg-bg-secondary text-text-primary rounded-lg border border-border focus:border-primary-light focus:outline-none placeholder-text-secondary/50 transition-theme text-sm"
|
|
80
|
-
aria-label="Search"
|
|
81
|
-
/>
|
|
82
|
-
</div>
|
|
83
|
-
|
|
84
|
-
{/* Categories */}
|
|
85
|
-
<div>
|
|
86
|
-
<h3 className="text-sm font-semibold text-text-primary mb-3">Categories</h3>
|
|
87
|
-
<div className="space-y-1">
|
|
88
|
-
{categories.map((category) => {
|
|
89
|
-
const Icon = category.icon;
|
|
90
|
-
const isActive = selectedCategory === category.id;
|
|
91
|
-
return (
|
|
92
|
-
<button
|
|
93
|
-
key={category.id}
|
|
94
|
-
onClick={() => onCategoryChange(category.id)}
|
|
95
|
-
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-all transition-theme ${
|
|
96
|
-
isActive
|
|
97
|
-
? 'bg-primary-light text-text-primary font-medium'
|
|
98
|
-
: 'text-text-secondary hover:bg-bg-secondary hover:text-text-primary'
|
|
99
|
-
}`}
|
|
100
|
-
type="button"
|
|
101
|
-
aria-pressed={isActive}
|
|
102
|
-
>
|
|
103
|
-
<Icon size={16} />
|
|
104
|
-
<span className="flex-1 text-left">{category.name}</span>
|
|
105
|
-
<span className={`text-xs ${isActive ? 'text-text-primary' : 'text-text-secondary'}`}>
|
|
106
|
-
{category.count}
|
|
107
|
-
</span>
|
|
108
|
-
</button>
|
|
109
|
-
);
|
|
110
|
-
})}
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
|
|
114
|
-
{/* Popular Tags */}
|
|
115
|
-
<div>
|
|
116
|
-
<h3 className="text-sm font-semibold text-text-primary mb-3">Popular Tags</h3>
|
|
117
|
-
<div className="flex flex-wrap gap-2">
|
|
118
|
-
{popularTags.map((tag) => (
|
|
119
|
-
<button
|
|
120
|
-
key={tag}
|
|
121
|
-
onClick={() => onTagToggle(tag)}
|
|
122
|
-
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-all transition-theme ${
|
|
123
|
-
selectedTags.includes(tag)
|
|
124
|
-
? 'bg-primary-light text-text-primary'
|
|
125
|
-
: 'bg-bg-secondary text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
|
|
126
|
-
}`}
|
|
127
|
-
type="button"
|
|
128
|
-
aria-pressed={selectedTags.includes(tag)}
|
|
129
|
-
>
|
|
130
|
-
{tag}
|
|
131
|
-
</button>
|
|
132
|
-
))}
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
135
|
-
|
|
136
|
-
{/* Sort */}
|
|
137
|
-
<div>
|
|
138
|
-
<h3 className="text-sm font-semibold text-text-primary mb-3">Sort By</h3>
|
|
139
|
-
<select
|
|
140
|
-
value={sortBy}
|
|
141
|
-
onChange={(e) => onSortChange(e.target.value)}
|
|
142
|
-
className="w-full px-3 py-2.5 bg-bg-secondary text-text-primary rounded-lg border border-border focus:border-primary-light focus:outline-none transition-theme text-sm"
|
|
143
|
-
aria-label="Sort by"
|
|
144
|
-
>
|
|
145
|
-
{sortOptions.map((option) => (
|
|
146
|
-
<option key={option.id} value={option.id}>
|
|
147
|
-
{option.name}
|
|
148
|
-
</option>
|
|
149
|
-
))}
|
|
150
|
-
</select>
|
|
151
|
-
</div>
|
|
152
|
-
|
|
153
|
-
{/* Additional Content */}
|
|
154
|
-
{children}
|
|
155
|
-
|
|
156
|
-
{/* Clear Filters */}
|
|
157
|
-
{hasActiveFilters && (
|
|
158
|
-
<button
|
|
159
|
-
onClick={onClearFilters}
|
|
160
|
-
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-bg-secondary text-text-secondary rounded-lg border border-border hover:border-primary-light hover:text-text-primary transition-all text-sm font-medium transition-theme"
|
|
161
|
-
type="button"
|
|
162
|
-
>
|
|
163
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
164
|
-
<path d="M18 6L6 18M6 6l12 12" />
|
|
165
|
-
</svg>
|
|
166
|
-
Clear All Filters
|
|
167
|
-
</button>
|
|
168
|
-
)}
|
|
169
|
-
</div>
|
|
170
|
-
</aside>
|
|
171
|
-
);
|
|
172
|
-
};
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Footer Organism Component
|
|
3
|
-
* @description Minimal footer with brand and social icons - optimized for performance
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { forwardRef, type HTMLAttributes, type ReactNode, memo } from 'react';
|
|
7
|
-
import React from 'react';
|
|
8
|
-
import { cn } from '../../infrastructure/utils';
|
|
9
|
-
import type { BaseProps } from '../../domain/types';
|
|
10
|
-
|
|
11
|
-
export interface FooterProps extends HTMLAttributes<HTMLElement>, BaseProps {
|
|
12
|
-
brand?: {
|
|
13
|
-
name: string;
|
|
14
|
-
};
|
|
15
|
-
social?: Array<{
|
|
16
|
-
name: string;
|
|
17
|
-
href: string;
|
|
18
|
-
icon: ReactNode;
|
|
19
|
-
}>;
|
|
20
|
-
copyright?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type SocialItem = {
|
|
24
|
-
name: string;
|
|
25
|
-
href: string;
|
|
26
|
-
icon: ReactNode;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
// Memoize social icon component to prevent unnecessary re-renders
|
|
30
|
-
const SocialIcon = memo<{
|
|
31
|
-
item: SocialItem;
|
|
32
|
-
}>(({ item }) => (
|
|
33
|
-
<a
|
|
34
|
-
href={item.href}
|
|
35
|
-
target="_blank"
|
|
36
|
-
rel="noopener noreferrer"
|
|
37
|
-
className="flex items-center justify-center w-10 h-10 rounded-lg bg-bg-secondary/50 text-text-secondary hover:bg-primary-gradient hover:text-white transition-all duration-200"
|
|
38
|
-
aria-label={item.name}
|
|
39
|
-
title={item.name}
|
|
40
|
-
>
|
|
41
|
-
<span className="relative">{item.icon}</span>
|
|
42
|
-
</a>
|
|
43
|
-
));
|
|
44
|
-
|
|
45
|
-
SocialIcon.displayName = 'SocialIcon';
|
|
46
|
-
|
|
47
|
-
export const Footer = memo(forwardRef<HTMLElement, FooterProps>(
|
|
48
|
-
({ className, brand, social, copyright = '© 2026 UmitUZ. All rights reserved.', ...props }, ref) => {
|
|
49
|
-
return (
|
|
50
|
-
<footer
|
|
51
|
-
ref={ref}
|
|
52
|
-
className={cn('relative mt-8 transition-theme', className)}
|
|
53
|
-
{...props}
|
|
54
|
-
>
|
|
55
|
-
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
56
|
-
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
|
|
57
|
-
{/* Brand */}
|
|
58
|
-
{brand && (
|
|
59
|
-
<div className="text-xl font-bold text-text-primary">
|
|
60
|
-
{brand.name}
|
|
61
|
-
</div>
|
|
62
|
-
)}
|
|
63
|
-
|
|
64
|
-
{/* Social Icons */}
|
|
65
|
-
{social && social.length > 0 && (
|
|
66
|
-
<div className="flex items-center gap-3">
|
|
67
|
-
{social.map((item, index) => (
|
|
68
|
-
<SocialIcon key={`${item.name}-${index}`} item={item} />
|
|
69
|
-
))}
|
|
70
|
-
</div>
|
|
71
|
-
)}
|
|
72
|
-
</div>
|
|
73
|
-
|
|
74
|
-
{/* Copyright */}
|
|
75
|
-
{copyright && (
|
|
76
|
-
<div className="text-center mt-6 pt-6 border-t border-border/20">
|
|
77
|
-
<p className="text-sm text-text-secondary/60">{copyright}</p>
|
|
78
|
-
</div>
|
|
79
|
-
)}
|
|
80
|
-
</div>
|
|
81
|
-
</footer>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
));
|
|
85
|
-
|
|
86
|
-
Footer.displayName = 'Footer';
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FormModal Component (Template)
|
|
3
|
-
* @description Pre-configured modal with form wrapper and loading state
|
|
4
|
-
* Reduces boilerplate in modal components like CampaignModal, ProjectListModal
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { forwardRef, type ReactNode } from 'react';
|
|
8
|
-
import { cn } from '../../infrastructure/utils';
|
|
9
|
-
import {
|
|
10
|
-
Dialog,
|
|
11
|
-
DialogContent,
|
|
12
|
-
DialogDescription,
|
|
13
|
-
DialogFooter,
|
|
14
|
-
DialogHeader,
|
|
15
|
-
DialogTitle,
|
|
16
|
-
} from './Dialog';
|
|
17
|
-
import { Button } from '../atoms';
|
|
18
|
-
import { Loader2 } from 'lucide-react';
|
|
19
|
-
import type { BaseProps } from '../../domain/types';
|
|
20
|
-
|
|
21
|
-
export interface FormModalProps extends BaseProps {
|
|
22
|
-
open: boolean;
|
|
23
|
-
onOpenChange: (open: boolean) => void;
|
|
24
|
-
title: string;
|
|
25
|
-
description?: string;
|
|
26
|
-
children: ReactNode;
|
|
27
|
-
onSubmit?: (e: React.FormEvent) => void | Promise<void>;
|
|
28
|
-
loading?: boolean;
|
|
29
|
-
submitText?: string;
|
|
30
|
-
cancelText?: string;
|
|
31
|
-
showFooter?: boolean;
|
|
32
|
-
footer?: ReactNode;
|
|
33
|
-
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
34
|
-
preventCloseOnSubmit?: boolean;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const sizeStyles = {
|
|
38
|
-
sm: 'sm:max-w-[400px]',
|
|
39
|
-
md: 'sm:max-w-[425px]',
|
|
40
|
-
lg: 'sm:max-w-[600px]',
|
|
41
|
-
xl: 'sm:max-w-[800px]',
|
|
42
|
-
full: 'sm:max-w-[95vw]',
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export const FormModal = forwardRef<HTMLDivElement, FormModalProps>(
|
|
46
|
-
(
|
|
47
|
-
{
|
|
48
|
-
className,
|
|
49
|
-
open,
|
|
50
|
-
onOpenChange,
|
|
51
|
-
title,
|
|
52
|
-
description,
|
|
53
|
-
children,
|
|
54
|
-
onSubmit,
|
|
55
|
-
loading = false,
|
|
56
|
-
submitText = 'Submit',
|
|
57
|
-
cancelText = 'Cancel',
|
|
58
|
-
showFooter = true,
|
|
59
|
-
footer,
|
|
60
|
-
size = 'md',
|
|
61
|
-
preventCloseOnSubmit = false,
|
|
62
|
-
...props
|
|
63
|
-
},
|
|
64
|
-
ref
|
|
65
|
-
) => {
|
|
66
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
67
|
-
e.preventDefault();
|
|
68
|
-
if (onSubmit && !loading) {
|
|
69
|
-
await onSubmit(e);
|
|
70
|
-
if (!preventCloseOnSubmit) {
|
|
71
|
-
onOpenChange(false);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
78
|
-
<DialogContent className={cn(sizeStyles[size], className)} ref={ref} {...props}>
|
|
79
|
-
{onSubmit ? (
|
|
80
|
-
<form onSubmit={handleSubmit}>
|
|
81
|
-
<DialogHeader>
|
|
82
|
-
<DialogTitle>{title}</DialogTitle>
|
|
83
|
-
{description && <DialogDescription>{description}</DialogDescription>}
|
|
84
|
-
</DialogHeader>
|
|
85
|
-
<div className="grid gap-4 py-4">{children}</div>
|
|
86
|
-
{showFooter && (
|
|
87
|
-
<DialogFooter>
|
|
88
|
-
{footer || (
|
|
89
|
-
<>
|
|
90
|
-
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={loading}>
|
|
91
|
-
{cancelText}
|
|
92
|
-
</Button>
|
|
93
|
-
<Button type="submit" disabled={loading}>
|
|
94
|
-
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
95
|
-
{submitText}
|
|
96
|
-
</Button>
|
|
97
|
-
</>
|
|
98
|
-
)}
|
|
99
|
-
</DialogFooter>
|
|
100
|
-
)}
|
|
101
|
-
</form>
|
|
102
|
-
) : (
|
|
103
|
-
<>
|
|
104
|
-
<DialogHeader>
|
|
105
|
-
<DialogTitle>{title}</DialogTitle>
|
|
106
|
-
{description && <DialogDescription>{description}</DialogDescription>}
|
|
107
|
-
</DialogHeader>
|
|
108
|
-
<div className="grid gap-4 py-4">{children}</div>
|
|
109
|
-
{showFooter && (
|
|
110
|
-
<DialogFooter>
|
|
111
|
-
{footer || (
|
|
112
|
-
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={loading}>
|
|
113
|
-
{cancelText}
|
|
114
|
-
</Button>
|
|
115
|
-
)}
|
|
116
|
-
</DialogFooter>
|
|
117
|
-
)}
|
|
118
|
-
</>
|
|
119
|
-
)}
|
|
120
|
-
</DialogContent>
|
|
121
|
-
</Dialog>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
FormModal.displayName = 'FormModal';
|