@liiift-studio/sanity-font-manager 2.3.18 → 2.4.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 +437 -437
- package/dist/index.js +103 -48
- package/dist/index.mjs +103 -48
- package/package.json +85 -85
- package/src/components/BatchUploadFonts.jsx +640 -639
- package/src/components/FontScriptUploaderComponent.jsx +463 -463
- package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
- package/src/components/KeyValueInput.jsx +95 -95
- package/src/components/KeyValueReferenceInput.jsx +254 -254
- package/src/components/NestedObjectArraySelector.jsx +146 -146
- package/src/components/PriceInput.jsx +26 -26
- package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
- package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
- package/src/components/SetOTF.jsx +87 -87
- package/src/components/SingleUploaderTool.jsx +673 -673
- package/src/components/StatusDisplay.jsx +26 -26
- package/src/components/StyleCountInput.jsx +16 -16
- package/src/components/UpdateScriptsComponent.jsx +76 -76
- package/src/components/UploadButton.jsx +43 -43
- package/src/components/UploadScriptsComponent.jsx +537 -537
- package/src/components/VariableInstanceReferencesInput.jsx +190 -190
- package/src/hooks/useNestedObjects.js +92 -92
- package/src/hooks/useSanityClient.js +9 -9
- package/src/index.js +70 -70
- package/src/schema/openTypeField.js +1945 -1945
- package/src/schema/styleCountField.js +12 -12
- package/src/schema/stylesField.js +268 -268
- package/src/schema/stylisticSetField.js +301 -301
- package/src/utils/generateCssFile.js +205 -205
- package/src/utils/generateFontData.js +145 -145
- package/src/utils/generateFontFile.js +38 -38
- package/src/utils/generateKeywords.js +185 -185
- package/src/utils/generateSubset.js +45 -45
- package/src/utils/getEmptyFontKit.js +99 -99
- package/src/utils/parseVariableFontInstances.js +211 -211
- package/src/utils/processFontFiles.js +487 -477
- package/src/utils/regenerateFontData.js +146 -146
- package/src/utils/sanitizeForSanityId.js +65 -65
- package/src/utils/updateFontPrices.js +94 -94
- package/src/utils/updateTypefaceDocument.js +149 -160
- package/src/utils/uploadFontFiles.js +115 -26
- package/src/utils/utils.js +24 -24
|
@@ -1,146 +1,146 @@
|
|
|
1
|
-
// Generic checkbox-list selector for nested objects from Sanity documents — configured via schema options
|
|
2
|
-
|
|
3
|
-
import React, { useMemo, useState } from 'react';
|
|
4
|
-
import { Stack, Card, Text, Checkbox, Box, Spinner, Flex } from '@sanity/ui';
|
|
5
|
-
import { useNestedObjects } from '../hooks/useNestedObjects';
|
|
6
|
-
import { set, unset } from 'sanity';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Sanity input component that renders a searchable checkbox list of items fetched
|
|
10
|
-
* from nested arrays inside Sanity documents.
|
|
11
|
-
*
|
|
12
|
-
* Configure via schema options:
|
|
13
|
-
* ```js
|
|
14
|
-
* components: { input: NestedObjectArraySelector },
|
|
15
|
-
* options: {
|
|
16
|
-
* sourceType: 'licenseGroup', // document type to query
|
|
17
|
-
* nestedField: 'sections', // array field to extract
|
|
18
|
-
* titleField: 'title', // GROQ expression for display text
|
|
19
|
-
* valueField: 'slug.current', // GROQ expression for stored value
|
|
20
|
-
* filter: 'state == "published"', // optional GROQ filter
|
|
21
|
-
* sortBy: 'title asc', // optional sort field + direction
|
|
22
|
-
* emptyMessage: 'No options found', // optional empty state text
|
|
23
|
-
* searchPlaceholder: 'Search...', // optional search input placeholder
|
|
24
|
-
* }
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
export const NestedObjectArraySelector = React.forwardRef((props, ref) => {
|
|
28
|
-
const { value = [], onChange, schemaType } = props;
|
|
29
|
-
|
|
30
|
-
const options = schemaType?.options || {};
|
|
31
|
-
const {
|
|
32
|
-
sourceType,
|
|
33
|
-
nestedField,
|
|
34
|
-
titleField,
|
|
35
|
-
valueField,
|
|
36
|
-
filter = '',
|
|
37
|
-
sortBy = 'title asc',
|
|
38
|
-
emptyMessage = 'No options found',
|
|
39
|
-
searchPlaceholder = 'Search...',
|
|
40
|
-
} = options;
|
|
41
|
-
|
|
42
|
-
const [searchTerm, setSearchTerm] = useState('');
|
|
43
|
-
const { objects, loading, error } = useNestedObjects({ sourceType, nestedField, titleField, valueField, filter, sortBy });
|
|
44
|
-
|
|
45
|
-
const filteredObjects = useMemo(() => {
|
|
46
|
-
if (!searchTerm) return objects;
|
|
47
|
-
const lower = searchTerm.toLowerCase();
|
|
48
|
-
return objects.filter(obj => obj.title?.toLowerCase().includes(lower));
|
|
49
|
-
}, [objects, searchTerm]);
|
|
50
|
-
|
|
51
|
-
const handleToggle = (itemValue) => {
|
|
52
|
-
const currentValue = value || [];
|
|
53
|
-
const isSelected = currentValue.includes(itemValue);
|
|
54
|
-
if (isSelected) {
|
|
55
|
-
const newValue = currentValue.filter(v => v !== itemValue);
|
|
56
|
-
onChange(newValue.length > 0 ? set(newValue) : unset());
|
|
57
|
-
} else {
|
|
58
|
-
onChange(set([...currentValue, itemValue]));
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
if (!sourceType || !nestedField || !titleField || !valueField) {
|
|
63
|
-
return (
|
|
64
|
-
<Card padding={3} tone="critical" border>
|
|
65
|
-
<Text size={1}>Configuration error: Missing required options (sourceType, nestedField, titleField, or valueField)</Text>
|
|
66
|
-
</Card>
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (loading) {
|
|
71
|
-
return (
|
|
72
|
-
<Card padding={3} border>
|
|
73
|
-
<Flex align="center" justify="center" padding={4}>
|
|
74
|
-
<Spinner />
|
|
75
|
-
<Box marginLeft={3}><Text size={1}>Loading options...</Text></Box>
|
|
76
|
-
</Flex>
|
|
77
|
-
</Card>
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (error) {
|
|
82
|
-
return (
|
|
83
|
-
<Card padding={3} tone="critical" border>
|
|
84
|
-
<Text size={1}>Error loading options: {error.message}</Text>
|
|
85
|
-
</Card>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (objects.length === 0) {
|
|
90
|
-
return (
|
|
91
|
-
<Card padding={3} tone="transparent" border>
|
|
92
|
-
<Text size={1} muted>{emptyMessage}</Text>
|
|
93
|
-
</Card>
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<Card padding={0} border ref={ref}>
|
|
99
|
-
{objects.length > 5 && (
|
|
100
|
-
<Box padding={3} style={{ borderBottom: '1px solid var(--card-border-color)' }}>
|
|
101
|
-
<input
|
|
102
|
-
type="text"
|
|
103
|
-
placeholder={searchPlaceholder}
|
|
104
|
-
value={searchTerm}
|
|
105
|
-
onChange={(e) => setSearchTerm(e.target.value)}
|
|
106
|
-
style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--card-border-color)', borderRadius: '4px', fontSize: '13px', fontFamily: 'inherit' }}
|
|
107
|
-
/>
|
|
108
|
-
</Box>
|
|
109
|
-
)}
|
|
110
|
-
|
|
111
|
-
<Stack space={0}>
|
|
112
|
-
{filteredObjects.length === 0 ? (
|
|
113
|
-
<Box padding={3}>
|
|
114
|
-
<Text size={1} muted>No results found for "{searchTerm}"</Text>
|
|
115
|
-
</Box>
|
|
116
|
-
) : (
|
|
117
|
-
filteredObjects.map((obj, index) => {
|
|
118
|
-
const isSelected = value?.includes(obj.value);
|
|
119
|
-
const isLast = index === filteredObjects.length - 1;
|
|
120
|
-
return (
|
|
121
|
-
<Box
|
|
122
|
-
key={obj.value}
|
|
123
|
-
padding={3}
|
|
124
|
-
style={{ borderBottom: isLast ? 'none' : '1px solid var(--card-border-color)', cursor: 'pointer', backgroundColor: isSelected ? 'var(--card-muted-fg-color)' : 'transparent', transition: 'background-color 0.2s' }}
|
|
125
|
-
onClick={() => handleToggle(obj.value)}
|
|
126
|
-
>
|
|
127
|
-
<Flex align="center" gap={3}>
|
|
128
|
-
<Checkbox checked={isSelected} readOnly style={{ pointerEvents: 'none' }} />
|
|
129
|
-
<Text size={1} weight={isSelected ? 'semibold' : 'regular'}>{obj.title}</Text>
|
|
130
|
-
</Flex>
|
|
131
|
-
</Box>
|
|
132
|
-
);
|
|
133
|
-
})
|
|
134
|
-
)}
|
|
135
|
-
</Stack>
|
|
136
|
-
|
|
137
|
-
{value?.length > 0 && (
|
|
138
|
-
<Box padding={2} paddingX={3} style={{ borderTop: '1px solid var(--card-border-color)', backgroundColor: 'var(--card-muted-fg-color)' }}>
|
|
139
|
-
<Text size={1} muted>{value.length} selected</Text>
|
|
140
|
-
</Box>
|
|
141
|
-
)}
|
|
142
|
-
</Card>
|
|
143
|
-
);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
NestedObjectArraySelector.displayName = 'NestedObjectArraySelector';
|
|
1
|
+
// Generic checkbox-list selector for nested objects from Sanity documents — configured via schema options
|
|
2
|
+
|
|
3
|
+
import React, { useMemo, useState } from 'react';
|
|
4
|
+
import { Stack, Card, Text, Checkbox, Box, Spinner, Flex } from '@sanity/ui';
|
|
5
|
+
import { useNestedObjects } from '../hooks/useNestedObjects';
|
|
6
|
+
import { set, unset } from 'sanity';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sanity input component that renders a searchable checkbox list of items fetched
|
|
10
|
+
* from nested arrays inside Sanity documents.
|
|
11
|
+
*
|
|
12
|
+
* Configure via schema options:
|
|
13
|
+
* ```js
|
|
14
|
+
* components: { input: NestedObjectArraySelector },
|
|
15
|
+
* options: {
|
|
16
|
+
* sourceType: 'licenseGroup', // document type to query
|
|
17
|
+
* nestedField: 'sections', // array field to extract
|
|
18
|
+
* titleField: 'title', // GROQ expression for display text
|
|
19
|
+
* valueField: 'slug.current', // GROQ expression for stored value
|
|
20
|
+
* filter: 'state == "published"', // optional GROQ filter
|
|
21
|
+
* sortBy: 'title asc', // optional sort field + direction
|
|
22
|
+
* emptyMessage: 'No options found', // optional empty state text
|
|
23
|
+
* searchPlaceholder: 'Search...', // optional search input placeholder
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export const NestedObjectArraySelector = React.forwardRef((props, ref) => {
|
|
28
|
+
const { value = [], onChange, schemaType } = props;
|
|
29
|
+
|
|
30
|
+
const options = schemaType?.options || {};
|
|
31
|
+
const {
|
|
32
|
+
sourceType,
|
|
33
|
+
nestedField,
|
|
34
|
+
titleField,
|
|
35
|
+
valueField,
|
|
36
|
+
filter = '',
|
|
37
|
+
sortBy = 'title asc',
|
|
38
|
+
emptyMessage = 'No options found',
|
|
39
|
+
searchPlaceholder = 'Search...',
|
|
40
|
+
} = options;
|
|
41
|
+
|
|
42
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
43
|
+
const { objects, loading, error } = useNestedObjects({ sourceType, nestedField, titleField, valueField, filter, sortBy });
|
|
44
|
+
|
|
45
|
+
const filteredObjects = useMemo(() => {
|
|
46
|
+
if (!searchTerm) return objects;
|
|
47
|
+
const lower = searchTerm.toLowerCase();
|
|
48
|
+
return objects.filter(obj => obj.title?.toLowerCase().includes(lower));
|
|
49
|
+
}, [objects, searchTerm]);
|
|
50
|
+
|
|
51
|
+
const handleToggle = (itemValue) => {
|
|
52
|
+
const currentValue = value || [];
|
|
53
|
+
const isSelected = currentValue.includes(itemValue);
|
|
54
|
+
if (isSelected) {
|
|
55
|
+
const newValue = currentValue.filter(v => v !== itemValue);
|
|
56
|
+
onChange(newValue.length > 0 ? set(newValue) : unset());
|
|
57
|
+
} else {
|
|
58
|
+
onChange(set([...currentValue, itemValue]));
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (!sourceType || !nestedField || !titleField || !valueField) {
|
|
63
|
+
return (
|
|
64
|
+
<Card padding={3} tone="critical" border>
|
|
65
|
+
<Text size={1}>Configuration error: Missing required options (sourceType, nestedField, titleField, or valueField)</Text>
|
|
66
|
+
</Card>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (loading) {
|
|
71
|
+
return (
|
|
72
|
+
<Card padding={3} border>
|
|
73
|
+
<Flex align="center" justify="center" padding={4}>
|
|
74
|
+
<Spinner />
|
|
75
|
+
<Box marginLeft={3}><Text size={1}>Loading options...</Text></Box>
|
|
76
|
+
</Flex>
|
|
77
|
+
</Card>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (error) {
|
|
82
|
+
return (
|
|
83
|
+
<Card padding={3} tone="critical" border>
|
|
84
|
+
<Text size={1}>Error loading options: {error.message}</Text>
|
|
85
|
+
</Card>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (objects.length === 0) {
|
|
90
|
+
return (
|
|
91
|
+
<Card padding={3} tone="transparent" border>
|
|
92
|
+
<Text size={1} muted>{emptyMessage}</Text>
|
|
93
|
+
</Card>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Card padding={0} border ref={ref}>
|
|
99
|
+
{objects.length > 5 && (
|
|
100
|
+
<Box padding={3} style={{ borderBottom: '1px solid var(--card-border-color)' }}>
|
|
101
|
+
<input
|
|
102
|
+
type="text"
|
|
103
|
+
placeholder={searchPlaceholder}
|
|
104
|
+
value={searchTerm}
|
|
105
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
106
|
+
style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--card-border-color)', borderRadius: '4px', fontSize: '13px', fontFamily: 'inherit' }}
|
|
107
|
+
/>
|
|
108
|
+
</Box>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
<Stack space={0}>
|
|
112
|
+
{filteredObjects.length === 0 ? (
|
|
113
|
+
<Box padding={3}>
|
|
114
|
+
<Text size={1} muted>No results found for "{searchTerm}"</Text>
|
|
115
|
+
</Box>
|
|
116
|
+
) : (
|
|
117
|
+
filteredObjects.map((obj, index) => {
|
|
118
|
+
const isSelected = value?.includes(obj.value);
|
|
119
|
+
const isLast = index === filteredObjects.length - 1;
|
|
120
|
+
return (
|
|
121
|
+
<Box
|
|
122
|
+
key={obj.value}
|
|
123
|
+
padding={3}
|
|
124
|
+
style={{ borderBottom: isLast ? 'none' : '1px solid var(--card-border-color)', cursor: 'pointer', backgroundColor: isSelected ? 'var(--card-muted-fg-color)' : 'transparent', transition: 'background-color 0.2s' }}
|
|
125
|
+
onClick={() => handleToggle(obj.value)}
|
|
126
|
+
>
|
|
127
|
+
<Flex align="center" gap={3}>
|
|
128
|
+
<Checkbox checked={isSelected} readOnly style={{ pointerEvents: 'none' }} />
|
|
129
|
+
<Text size={1} weight={isSelected ? 'semibold' : 'regular'}>{obj.title}</Text>
|
|
130
|
+
</Flex>
|
|
131
|
+
</Box>
|
|
132
|
+
);
|
|
133
|
+
})
|
|
134
|
+
)}
|
|
135
|
+
</Stack>
|
|
136
|
+
|
|
137
|
+
{value?.length > 0 && (
|
|
138
|
+
<Box padding={2} paddingX={3} style={{ borderTop: '1px solid var(--card-border-color)', backgroundColor: 'var(--card-muted-fg-color)' }}>
|
|
139
|
+
<Text size={1} muted>{value.length} selected</Text>
|
|
140
|
+
</Box>
|
|
141
|
+
)}
|
|
142
|
+
</Card>
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
NestedObjectArraySelector.displayName = 'NestedObjectArraySelector';
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
// Reusable $ + number input for collection and pair price fields
|
|
2
|
-
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import { Flex, Text } from '@sanity/ui';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Renders an inline price field: "Price $ [input] per style"
|
|
8
|
-
* @param {Object} props
|
|
9
|
-
* @param {string} props.inputPrice - Current price value
|
|
10
|
-
* @param {Function} props.handleInputChange - onChange handler
|
|
11
|
-
*/
|
|
12
|
-
const PriceInput = ({ inputPrice, handleInputChange }) => (
|
|
13
|
-
<Flex align="center" gap={2}>
|
|
14
|
-
<Text size={1} muted>Price:</Text>
|
|
15
|
-
<Text size={1} muted>$</Text>
|
|
16
|
-
<input
|
|
17
|
-
value={inputPrice}
|
|
18
|
-
onChange={handleInputChange}
|
|
19
|
-
type="number"
|
|
20
|
-
style={{ textAlign: 'end', padding: '5px', maxWidth: '75px' }}
|
|
21
|
-
/>
|
|
22
|
-
<Text size={1} muted>per style</Text>
|
|
23
|
-
</Flex>
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
export default PriceInput;
|
|
1
|
+
// Reusable $ + number input for collection and pair price fields
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Flex, Text } from '@sanity/ui';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Renders an inline price field: "Price $ [input] per style"
|
|
8
|
+
* @param {Object} props
|
|
9
|
+
* @param {string} props.inputPrice - Current price value
|
|
10
|
+
* @param {Function} props.handleInputChange - onChange handler
|
|
11
|
+
*/
|
|
12
|
+
const PriceInput = ({ inputPrice, handleInputChange }) => (
|
|
13
|
+
<Flex align="center" gap={2}>
|
|
14
|
+
<Text size={1} muted>Price:</Text>
|
|
15
|
+
<Text size={1} muted>$</Text>
|
|
16
|
+
<input
|
|
17
|
+
value={inputPrice}
|
|
18
|
+
onChange={handleInputChange}
|
|
19
|
+
type="number"
|
|
20
|
+
style={{ textAlign: 'end', padding: '5px', maxWidth: '75px' }}
|
|
21
|
+
/>
|
|
22
|
+
<Text size={1} muted>per style</Text>
|
|
23
|
+
</Flex>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export default PriceInput;
|
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
// Generates a single primary full-family collection from a typeface's linked fonts and prepends it to the existing collections array
|
|
2
|
-
|
|
3
|
-
import React, { useCallback, useState } from 'react';
|
|
4
|
-
import { Stack, Flex, Text, Button, Card, Spinner } from '@sanity/ui';
|
|
5
|
-
import { useFormValue } from 'sanity';
|
|
6
|
-
import { nanoid } from 'nanoid';
|
|
7
|
-
|
|
8
|
-
import { useSanityClient } from '../hooks/useSanityClient';
|
|
9
|
-
import StatusDisplay from './StatusDisplay';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Generates a full-family collection document from the typeface's linked fonts
|
|
13
|
-
* and prepends it to the existing styles.collections array.
|
|
14
|
-
*/
|
|
15
|
-
export const PrimaryCollectionGeneratorTypeface = () => {
|
|
16
|
-
const client = useSanityClient();
|
|
17
|
-
|
|
18
|
-
const [status, setStatus] = useState('ready');
|
|
19
|
-
const [ready, setReady] = useState(true);
|
|
20
|
-
const [price, setPrice] = useState(
|
|
21
|
-
process.env.SANITY_STUDIO_DEFAULT_COLLECTION_PRICE || 100
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
const fonts = useFormValue(['styles', 'fonts']);
|
|
25
|
-
const title = useFormValue(['title']);
|
|
26
|
-
const preferredStyle = useFormValue(['preferredStyle']);
|
|
27
|
-
const docId = useFormValue(['_id']);
|
|
28
|
-
const styles = useFormValue(['styles']);
|
|
29
|
-
|
|
30
|
-
/** Creates or replaces the full-family collection document and prepends it to the typeface's collections array. */
|
|
31
|
-
const generateCollection = useCallback(async () => {
|
|
32
|
-
setStatus('Generating collection...');
|
|
33
|
-
setReady(false);
|
|
34
|
-
|
|
35
|
-
let id = title.toLowerCase().replace(/\s+/g, '-').slice(0, 200);
|
|
36
|
-
if (!id.includes('collection')) id += '-collection';
|
|
37
|
-
|
|
38
|
-
const colTitle = id.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
39
|
-
|
|
40
|
-
const collectionDoc = {
|
|
41
|
-
_key: nanoid(),
|
|
42
|
-
_id: id,
|
|
43
|
-
title: colTitle,
|
|
44
|
-
slug: { _type: 'slug', current: id },
|
|
45
|
-
price: Number(price) || 0,
|
|
46
|
-
fonts: Object.values(fonts),
|
|
47
|
-
preferredStyle: preferredStyle,
|
|
48
|
-
_type: 'collection',
|
|
49
|
-
type: 'collection',
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
const sanityCollection = await client.createOrReplace(collectionDoc);
|
|
54
|
-
const collections = styles.collections || [];
|
|
55
|
-
|
|
56
|
-
await client.patch(docId)
|
|
57
|
-
.setIfMissing({ styles: {} })
|
|
58
|
-
.set({
|
|
59
|
-
styles: {
|
|
60
|
-
...styles,
|
|
61
|
-
collections: [{
|
|
62
|
-
_type: 'reference',
|
|
63
|
-
_key: nanoid(),
|
|
64
|
-
_ref: sanityCollection._id,
|
|
65
|
-
_weak: true,
|
|
66
|
-
}, ...collections],
|
|
67
|
-
},
|
|
68
|
-
})
|
|
69
|
-
.commit();
|
|
70
|
-
|
|
71
|
-
setStatus('Collection generated');
|
|
72
|
-
} catch (err) {
|
|
73
|
-
console.error('Error creating collection:', err.message);
|
|
74
|
-
setStatus('Error generating collection');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
setReady(true);
|
|
78
|
-
}, [docId, fonts, price, preferredStyle, styles, title, client]);
|
|
79
|
-
|
|
80
|
-
if (!title || !fonts) return null;
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<Stack space={2}>
|
|
84
|
-
<StatusDisplay status={status} error={false} />
|
|
85
|
-
<Card border padding={2} shadow={1} radius={2}>
|
|
86
|
-
{ready ? (
|
|
87
|
-
<Stack space={3}>
|
|
88
|
-
<Flex align="center" gap={2} marginTop={1} marginBottom={1}>
|
|
89
|
-
<Text size={1} muted>Price</Text>
|
|
90
|
-
<Text size={1} muted>$</Text>
|
|
91
|
-
<input
|
|
92
|
-
value={price}
|
|
93
|
-
onChange={(e) => setPrice(e.target.value)}
|
|
94
|
-
type="number"
|
|
95
|
-
style={{ textAlign: 'end', padding: '5px', maxWidth: '75px' }}
|
|
96
|
-
/>
|
|
97
|
-
<Text size={1} muted>per full family</Text>
|
|
98
|
-
</Flex>
|
|
99
|
-
<Button
|
|
100
|
-
mode="ghost"
|
|
101
|
-
tone="primary"
|
|
102
|
-
style={{ width: '100%' }}
|
|
103
|
-
onClick={generateCollection}
|
|
104
|
-
text="Generate Full Family Collection"
|
|
105
|
-
/>
|
|
106
|
-
</Stack>
|
|
107
|
-
) : (
|
|
108
|
-
<Flex align="center" justify="center" gap={3} padding={4}>
|
|
109
|
-
<Spinner />
|
|
110
|
-
<Text muted size={1}>{status}</Text>
|
|
111
|
-
</Flex>
|
|
112
|
-
)}
|
|
113
|
-
</Card>
|
|
114
|
-
</Stack>
|
|
115
|
-
);
|
|
116
|
-
};
|
|
1
|
+
// Generates a single primary full-family collection from a typeface's linked fonts and prepends it to the existing collections array
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useState } from 'react';
|
|
4
|
+
import { Stack, Flex, Text, Button, Card, Spinner } from '@sanity/ui';
|
|
5
|
+
import { useFormValue } from 'sanity';
|
|
6
|
+
import { nanoid } from 'nanoid';
|
|
7
|
+
|
|
8
|
+
import { useSanityClient } from '../hooks/useSanityClient';
|
|
9
|
+
import StatusDisplay from './StatusDisplay';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generates a full-family collection document from the typeface's linked fonts
|
|
13
|
+
* and prepends it to the existing styles.collections array.
|
|
14
|
+
*/
|
|
15
|
+
export const PrimaryCollectionGeneratorTypeface = () => {
|
|
16
|
+
const client = useSanityClient();
|
|
17
|
+
|
|
18
|
+
const [status, setStatus] = useState('ready');
|
|
19
|
+
const [ready, setReady] = useState(true);
|
|
20
|
+
const [price, setPrice] = useState(
|
|
21
|
+
process.env.SANITY_STUDIO_DEFAULT_COLLECTION_PRICE || 100
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const fonts = useFormValue(['styles', 'fonts']);
|
|
25
|
+
const title = useFormValue(['title']);
|
|
26
|
+
const preferredStyle = useFormValue(['preferredStyle']);
|
|
27
|
+
const docId = useFormValue(['_id']);
|
|
28
|
+
const styles = useFormValue(['styles']);
|
|
29
|
+
|
|
30
|
+
/** Creates or replaces the full-family collection document and prepends it to the typeface's collections array. */
|
|
31
|
+
const generateCollection = useCallback(async () => {
|
|
32
|
+
setStatus('Generating collection...');
|
|
33
|
+
setReady(false);
|
|
34
|
+
|
|
35
|
+
let id = title.toLowerCase().replace(/\s+/g, '-').slice(0, 200);
|
|
36
|
+
if (!id.includes('collection')) id += '-collection';
|
|
37
|
+
|
|
38
|
+
const colTitle = id.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
39
|
+
|
|
40
|
+
const collectionDoc = {
|
|
41
|
+
_key: nanoid(),
|
|
42
|
+
_id: id,
|
|
43
|
+
title: colTitle,
|
|
44
|
+
slug: { _type: 'slug', current: id },
|
|
45
|
+
price: Number(price) || 0,
|
|
46
|
+
fonts: Object.values(fonts),
|
|
47
|
+
preferredStyle: preferredStyle,
|
|
48
|
+
_type: 'collection',
|
|
49
|
+
type: 'collection',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const sanityCollection = await client.createOrReplace(collectionDoc);
|
|
54
|
+
const collections = styles.collections || [];
|
|
55
|
+
|
|
56
|
+
await client.patch(docId)
|
|
57
|
+
.setIfMissing({ styles: {} })
|
|
58
|
+
.set({
|
|
59
|
+
styles: {
|
|
60
|
+
...styles,
|
|
61
|
+
collections: [{
|
|
62
|
+
_type: 'reference',
|
|
63
|
+
_key: nanoid(),
|
|
64
|
+
_ref: sanityCollection._id,
|
|
65
|
+
_weak: true,
|
|
66
|
+
}, ...collections],
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
.commit();
|
|
70
|
+
|
|
71
|
+
setStatus('Collection generated');
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error('Error creating collection:', err.message);
|
|
74
|
+
setStatus('Error generating collection');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setReady(true);
|
|
78
|
+
}, [docId, fonts, price, preferredStyle, styles, title, client]);
|
|
79
|
+
|
|
80
|
+
if (!title || !fonts) return null;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Stack space={2}>
|
|
84
|
+
<StatusDisplay status={status} error={false} />
|
|
85
|
+
<Card border padding={2} shadow={1} radius={2}>
|
|
86
|
+
{ready ? (
|
|
87
|
+
<Stack space={3}>
|
|
88
|
+
<Flex align="center" gap={2} marginTop={1} marginBottom={1}>
|
|
89
|
+
<Text size={1} muted>Price</Text>
|
|
90
|
+
<Text size={1} muted>$</Text>
|
|
91
|
+
<input
|
|
92
|
+
value={price}
|
|
93
|
+
onChange={(e) => setPrice(e.target.value)}
|
|
94
|
+
type="number"
|
|
95
|
+
style={{ textAlign: 'end', padding: '5px', maxWidth: '75px' }}
|
|
96
|
+
/>
|
|
97
|
+
<Text size={1} muted>per full family</Text>
|
|
98
|
+
</Flex>
|
|
99
|
+
<Button
|
|
100
|
+
mode="ghost"
|
|
101
|
+
tone="primary"
|
|
102
|
+
style={{ width: '100%' }}
|
|
103
|
+
onClick={generateCollection}
|
|
104
|
+
text="Generate Full Family Collection"
|
|
105
|
+
/>
|
|
106
|
+
</Stack>
|
|
107
|
+
) : (
|
|
108
|
+
<Flex align="center" justify="center" gap={3} padding={4}>
|
|
109
|
+
<Spinner />
|
|
110
|
+
<Text muted size={1}>{status}</Text>
|
|
111
|
+
</Flex>
|
|
112
|
+
)}
|
|
113
|
+
</Card>
|
|
114
|
+
</Stack>
|
|
115
|
+
);
|
|
116
|
+
};
|