droplinked-editor-configs 1.9.9 → 1.9.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/productGrid/components/FilterPanel/PriceInput/PriceInput.d.ts +3 -2
- package/dist/droplinked-editor.es.js +5305 -5291
- package/dist/droplinked-editor.umd.js +43 -43
- package/dist/lib/stores/app/appStore.d.ts +3 -12
- package/dist/lib/utils/urlUtils.d.ts +11 -0
- package/package.json +1 -1
- package/src/components/SocialMediaBar/SocialMediaBar.tsx +18 -35
- package/src/components/header/Sidebar.tsx +1 -1
- package/src/components/productGrid/components/FilterPanel/PriceInput/PriceInput.tsx +9 -4
- package/src/components/productGrid/components/FilterPanel/PriceRangeFilter/PriceRangeFilter.tsx +25 -12
- package/src/lib/stores/app/appStore.ts +1 -1
- package/src/lib/utils/urlUtils.ts +40 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -12,10 +12,7 @@ export interface IAppStore {
|
|
|
12
12
|
states: {
|
|
13
13
|
cart: Icart;
|
|
14
14
|
user: {
|
|
15
|
-
[shopName: string]:
|
|
16
|
-
token: string;
|
|
17
|
-
user: IUser;
|
|
18
|
-
};
|
|
15
|
+
[shopName: string]: IUser;
|
|
19
16
|
} | null;
|
|
20
17
|
shop: IShop;
|
|
21
18
|
};
|
|
@@ -34,10 +31,7 @@ declare const useAppStore: import('zustand').UseBoundStore<Omit<Omit<import('zus
|
|
|
34
31
|
states: {
|
|
35
32
|
cart: Icart;
|
|
36
33
|
user: {
|
|
37
|
-
[shopName: string]:
|
|
38
|
-
token: string;
|
|
39
|
-
user: IUser;
|
|
40
|
-
};
|
|
34
|
+
[shopName: string]: IUser;
|
|
41
35
|
} | null;
|
|
42
36
|
shop: IShop;
|
|
43
37
|
};
|
|
@@ -51,10 +45,7 @@ declare const useAppStore: import('zustand').UseBoundStore<Omit<Omit<import('zus
|
|
|
51
45
|
states: {
|
|
52
46
|
cart: Icart;
|
|
53
47
|
user: {
|
|
54
|
-
[shopName: string]:
|
|
55
|
-
token: string;
|
|
56
|
-
user: IUser;
|
|
57
|
-
};
|
|
48
|
+
[shopName: string]: IUser;
|
|
58
49
|
} | null;
|
|
59
50
|
shop: IShop;
|
|
60
51
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if a given string is likely an external URL (domain or http/https).
|
|
3
|
+
* It returns true for "google.com", "www.test.com", "http://site.com".
|
|
4
|
+
* It returns false for "home", "/about", "contact-us".
|
|
5
|
+
*/
|
|
6
|
+
export declare const isExternalUrl: (url: string) => boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Normalizes a URL string.
|
|
9
|
+
* If it's detected as an external URL but missing protocol, prepends https://.
|
|
10
|
+
*/
|
|
11
|
+
export declare const normalizeUrl: (url: string) => string;
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { socialMediaLinks } from './socialLinks';
|
|
3
3
|
import CustomIcon from 'components/customIcon/CustomIcon';
|
|
4
4
|
import useThemeInfo from 'hooks/useThemeInfo';
|
|
5
|
+
import { isExternalUrl, normalizeUrl } from 'lib/utils/urlUtils';
|
|
5
6
|
|
|
6
7
|
// Accept socialChannels as prop
|
|
7
8
|
interface SocialChannel {
|
|
@@ -25,12 +26,11 @@ interface SocialMediaBarProps {
|
|
|
25
26
|
const SocialMediaBar: React.FC<SocialMediaBarProps> = React.memo(({ socialChannels }) => {
|
|
26
27
|
const { shopDesign: { foreground } } = useThemeInfo()
|
|
27
28
|
|
|
28
|
-
// Map channel name to
|
|
29
|
-
const
|
|
30
|
-
|
|
29
|
+
// Map channel name to social link item
|
|
30
|
+
const getSocialLinkItem = (channel: string) => {
|
|
31
|
+
return socialMediaLinks.find(
|
|
31
32
|
(item) => item.key.replace('URL', '').toLowerCase() === channel.toLowerCase()
|
|
32
33
|
);
|
|
33
|
-
return found ? found.icon : undefined;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
return (
|
|
@@ -39,12 +39,22 @@ const SocialMediaBar: React.FC<SocialMediaBarProps> = React.memo(({ socialChanne
|
|
|
39
39
|
.filter((item) => !!item.links?.url && !!item.links?.channel)
|
|
40
40
|
.map((item, idx) => {
|
|
41
41
|
const { channel, url } = item.links!;
|
|
42
|
-
const
|
|
43
|
-
if (!
|
|
42
|
+
const socialItem = getSocialLinkItem(channel!);
|
|
43
|
+
if (!socialItem) return null;
|
|
44
|
+
|
|
45
|
+
const Icon = socialItem.icon;
|
|
46
|
+
let href = url!;
|
|
47
|
+
|
|
48
|
+
if (isExternalUrl(href)) {
|
|
49
|
+
href = normalizeUrl(href);
|
|
50
|
+
} else {
|
|
51
|
+
href = `${socialItem.link}${href.replace(/^\//, '')}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
return (
|
|
45
55
|
<a
|
|
46
56
|
key={channel + idx}
|
|
47
|
-
href={
|
|
57
|
+
href={href}
|
|
48
58
|
target="_blank"
|
|
49
59
|
rel="noopener noreferrer"
|
|
50
60
|
className="inline-block rounded-md p-[10px] transition-colors duration-100 ease-out"
|
|
@@ -58,31 +68,4 @@ const SocialMediaBar: React.FC<SocialMediaBarProps> = React.memo(({ socialChanne
|
|
|
58
68
|
);
|
|
59
69
|
});
|
|
60
70
|
|
|
61
|
-
export default SocialMediaBar;
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Utility function to darken a hexadecimal color by a percentage
|
|
65
|
-
*
|
|
66
|
-
* @param color - Hexadecimal color string (e.g., "#FFFFFF")
|
|
67
|
-
* @param percent - Percentage to darken the color (-100 to 100)
|
|
68
|
-
* @returns Darkened hexadecimal color string
|
|
69
|
-
*/
|
|
70
|
-
const darkenColor = (color: string, percent: number): string => {
|
|
71
|
-
let num = parseInt(color.slice(1), 16);
|
|
72
|
-
let amt = Math.round(2.55 * percent);
|
|
73
|
-
let R = (num >> 16) + amt;
|
|
74
|
-
let G = ((num >> 8) & 0x00ff) + amt;
|
|
75
|
-
let B = (num & 0x0000ff) + amt;
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
'#' +
|
|
79
|
-
(
|
|
80
|
-
0x1000000 +
|
|
81
|
-
(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
|
|
82
|
-
(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
|
|
83
|
-
(B < 255 ? (B < 1 ? 0 : B) : 255)
|
|
84
|
-
)
|
|
85
|
-
.toString(16)
|
|
86
|
-
.slice(1)
|
|
87
|
-
);
|
|
88
|
-
};
|
|
71
|
+
export default SocialMediaBar;
|
|
@@ -18,7 +18,7 @@ interface Props {
|
|
|
18
18
|
function Sidebar({ iconColor, linkManagement, ProfileDropdownWrapper }: Props) {
|
|
19
19
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
|
20
20
|
const { states: { user, shop: { url } } } = useAppStore()
|
|
21
|
-
const { id } = user?.[url]
|
|
21
|
+
const { id } = user?.[url] || {}
|
|
22
22
|
const { shopDesign: { backgroundBody } } = useThemeInfo()
|
|
23
23
|
const [isLargerThan768] = useMediaQuery('(min-width: 768px)')
|
|
24
24
|
const { defineMultiStyleConfig } = createMultiStyleConfigHelpers(parts.keys)
|
|
@@ -4,8 +4,9 @@ import CurrencyIcon from './CurrencyIcon'
|
|
|
4
4
|
import useThemeInfo from 'hooks/useThemeInfo'
|
|
5
5
|
import usePriceInputClassNames from 'components/productGrid/hooks/usePriceInputClassNames'
|
|
6
6
|
|
|
7
|
-
interface Props extends ComponentProps<"input"> {
|
|
8
|
-
|
|
7
|
+
interface Props extends Omit<ComponentProps<"input">, 'value' | 'onChange'> {
|
|
8
|
+
value: number | string | undefined
|
|
9
|
+
onValueChange: (value: number | string) => void
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export default function PriceInput({ value, placeholder, onValueChange, ...inputProps }: Props) {
|
|
@@ -21,9 +22,13 @@ export default function PriceInput({ value, placeholder, onValueChange, ...input
|
|
|
21
22
|
<input
|
|
22
23
|
type="number"
|
|
23
24
|
className={` !font-shopfont ${inputClassName}`}
|
|
24
|
-
value={value}
|
|
25
|
+
value={value ?? ''}
|
|
25
26
|
placeholder={placeholder}
|
|
26
|
-
onChange={({ target: { validity, value } }) =>
|
|
27
|
+
onChange={({ target: { validity, value } }) => {
|
|
28
|
+
if (validity.valid) {
|
|
29
|
+
onValueChange(value === '' ? '' : +value)
|
|
30
|
+
}
|
|
31
|
+
}}
|
|
27
32
|
onKeyDown={(e) => {
|
|
28
33
|
const invalidKeys = ['+', '-', 'e', '.']
|
|
29
34
|
if (invalidKeys.includes(e.key)) e.preventDefault()
|
package/src/components/productGrid/components/FilterPanel/PriceRangeFilter/PriceRangeFilter.tsx
CHANGED
|
@@ -21,8 +21,8 @@ export default function PriceRangeFilter() {
|
|
|
21
21
|
} : {}
|
|
22
22
|
|
|
23
23
|
const { minPrice, maxPrice, setStagedProductQuery, applyStagedFilters } = useProductQueryStore(state => ({
|
|
24
|
-
minPrice: state.stagedProductQuery.minPrice
|
|
25
|
-
maxPrice: state.stagedProductQuery.maxPrice
|
|
24
|
+
minPrice: state.stagedProductQuery.minPrice,
|
|
25
|
+
maxPrice: state.stagedProductQuery.maxPrice,
|
|
26
26
|
setStagedProductQuery: state.setStagedProductQuery,
|
|
27
27
|
applyStagedFilters: state.applyStagedFilters
|
|
28
28
|
}))
|
|
@@ -31,15 +31,28 @@ export default function PriceRangeFilter() {
|
|
|
31
31
|
const debouncedMaxPrice = useDebounce(maxPrice, 300)
|
|
32
32
|
|
|
33
33
|
useEffect(() => {
|
|
34
|
+
if (typeof debouncedMinPrice === 'number' && typeof debouncedMaxPrice === 'number' && debouncedMinPrice > debouncedMaxPrice) {
|
|
35
|
+
setStagedProductQuery('minPrice', debouncedMaxPrice)
|
|
36
|
+
setStagedProductQuery('maxPrice', debouncedMinPrice)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
34
39
|
applyStagedFilters()
|
|
35
|
-
}, [debouncedMinPrice, debouncedMaxPrice, applyStagedFilters])
|
|
40
|
+
}, [debouncedMinPrice, debouncedMaxPrice, applyStagedFilters, setStagedProductQuery])
|
|
36
41
|
|
|
37
|
-
const handleMinChange = (newMin: number) => {
|
|
38
|
-
if (newMin
|
|
42
|
+
const handleMinChange = (newMin: number | string) => {
|
|
43
|
+
if (newMin === '') {
|
|
44
|
+
setStagedProductQuery('minPrice', undefined)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
if (typeof newMin === 'number') setStagedProductQuery('minPrice', newMin)
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
const handleMaxChange = (newMax: number) => {
|
|
42
|
-
if (newMax
|
|
50
|
+
const handleMaxChange = (newMax: number | string) => {
|
|
51
|
+
if (newMax === '') {
|
|
52
|
+
setStagedProductQuery('maxPrice', undefined)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
if (typeof newMax === 'number') setStagedProductQuery('maxPrice', newMax)
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
if (isLoading) return <FilterCheckboxSkeleton />
|
|
@@ -48,15 +61,15 @@ export default function PriceRangeFilter() {
|
|
|
48
61
|
<div className="flex flex-col gap-4">
|
|
49
62
|
<div className="flex items-center gap-3">
|
|
50
63
|
<PriceInput
|
|
51
|
-
value={minPrice}
|
|
52
|
-
placeholder=
|
|
64
|
+
value={minPrice ?? ''}
|
|
65
|
+
placeholder={min.toString()}
|
|
53
66
|
onValueChange={handleMinChange}
|
|
54
67
|
min={min}
|
|
55
68
|
/>
|
|
56
69
|
<PLPIcons.Separator className="shrink-0" />
|
|
57
70
|
<PriceInput
|
|
58
|
-
value={maxPrice}
|
|
59
|
-
placeholder=
|
|
71
|
+
value={maxPrice ?? ''}
|
|
72
|
+
placeholder={max.toString()}
|
|
60
73
|
onValueChange={handleMaxChange}
|
|
61
74
|
max={max}
|
|
62
75
|
/>
|
|
@@ -65,7 +78,7 @@ export default function PriceRangeFilter() {
|
|
|
65
78
|
className="price-range-slider"
|
|
66
79
|
thumbClassName={isDarkTheme ? "dark-price-range-slider__thumb" : "price-range-slider__thumb"}
|
|
67
80
|
trackClassName={isDarkTheme ? "dark-price-range-slider__track" : "price-range-slider__track"}
|
|
68
|
-
value={[minPrice, maxPrice]}
|
|
81
|
+
value={[minPrice ?? min, maxPrice ?? max]}
|
|
69
82
|
renderThumb={(props) => <div {...props} />}
|
|
70
83
|
minDistance={1}
|
|
71
84
|
onChange={([newMin, newMax]) => {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if a given string is likely an external URL (domain or http/https).
|
|
3
|
+
* It returns true for "google.com", "www.test.com", "http://site.com".
|
|
4
|
+
* It returns false for "home", "/about", "contact-us".
|
|
5
|
+
*/
|
|
6
|
+
export const isExternalUrl = (url: string): boolean => {
|
|
7
|
+
if (!url) return false;
|
|
8
|
+
|
|
9
|
+
// Already has protocol
|
|
10
|
+
if (/^https?:\/\//i.test(url)) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Check for domain-like pattern (e.g., "example.com", "sub.domain.net/path")
|
|
15
|
+
// Pattern: starts with alphanumeric, contains dots, ends with TLD (2+ chars)
|
|
16
|
+
// We exclude strings that start with '/' (absolute paths)
|
|
17
|
+
if (url.startsWith('/')) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Regex to match domain at the start of the string
|
|
22
|
+
// [domain part].[tld] followed by optional path/query
|
|
23
|
+
const domainRegex = /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(\/.*)?$/;
|
|
24
|
+
|
|
25
|
+
return domainRegex.test(url);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Normalizes a URL string.
|
|
30
|
+
* If it's detected as an external URL but missing protocol, prepends https://.
|
|
31
|
+
*/
|
|
32
|
+
export const normalizeUrl = (url: string): string => {
|
|
33
|
+
if (!url) return url;
|
|
34
|
+
|
|
35
|
+
if (isExternalUrl(url) && !/^https?:\/\//i.test(url)) {
|
|
36
|
+
return `https://${url}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return url;
|
|
40
|
+
};
|