@liiift-studio/sanity-font-manager 2.7.0 → 2.8.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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +695 -437
  3. package/dist/index.js +2330 -7084
  4. package/dist/index.mjs +2331 -7085
  5. package/package.json +84 -83
  6. package/src/components/BatchUploadFonts.jsx +655 -655
  7. package/src/components/BulkActions.jsx +99 -99
  8. package/src/components/ExistingDocumentResolver.jsx +152 -152
  9. package/src/components/FontReviewCard.jsx +455 -455
  10. package/src/components/FontScriptUploaderComponent.jsx +463 -463
  11. package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
  12. package/src/components/KeyValueInput.jsx +95 -95
  13. package/src/components/KeyValueReferenceInput.jsx +267 -267
  14. package/src/components/NestedObjectArraySelector.jsx +146 -146
  15. package/src/components/PriceInput.jsx +26 -26
  16. package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
  17. package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
  18. package/src/components/SetOTF.jsx +87 -87
  19. package/src/components/SingleUploaderTool.jsx +674 -674
  20. package/src/components/StatusDisplay.jsx +26 -26
  21. package/src/components/StyleCountInput.jsx +16 -16
  22. package/src/components/UpdateScriptsComponent.jsx +76 -76
  23. package/src/components/UploadButton.jsx +43 -43
  24. package/src/components/UploadModal.jsx +309 -309
  25. package/src/components/UploadScriptsComponent.jsx +539 -539
  26. package/src/components/UploadStep1Settings.jsx +272 -272
  27. package/src/components/UploadStep2Review.jsx +478 -478
  28. package/src/components/UploadStep3Execute.jsx +234 -234
  29. package/src/components/UploadStep3bInstances.jsx +396 -396
  30. package/src/components/UploadSummary.jsx +196 -196
  31. package/src/components/VariableInstanceReferencesInput.jsx +190 -190
  32. package/src/hooks/useNestedObjects.js +92 -92
  33. package/src/hooks/useSanityClient.js +9 -9
  34. package/src/index.js +120 -120
  35. package/src/schema/openTypeField.js +1995 -1995
  36. package/src/schema/styleCountField.js +12 -12
  37. package/src/schema/stylesField.js +302 -302
  38. package/src/schema/stylisticSetField.js +301 -301
  39. package/src/utils/buildUploadPlan.js +326 -326
  40. package/src/utils/executeUploadPlan.js +430 -430
  41. package/src/utils/executionReducer.js +56 -56
  42. package/src/utils/fontHelpers.js +281 -281
  43. package/src/utils/generateCssFile.js +207 -207
  44. package/src/utils/generateFontData.js +98 -98
  45. package/src/utils/generateFontFile.js +38 -38
  46. package/src/utils/generateKeywords.js +185 -185
  47. package/src/utils/generateSubset.js +45 -45
  48. package/src/utils/getEmptyFontKit.js +101 -101
  49. package/src/utils/parseFont.js +56 -56
  50. package/src/utils/parseVariableFontInstances.js +301 -301
  51. package/src/utils/planReducer.js +531 -531
  52. package/src/utils/planTypes.js +183 -183
  53. package/src/utils/processFontFiles.js +530 -530
  54. package/src/utils/regenerateFontData.js +146 -146
  55. package/src/utils/resolveExistingFont.js +87 -87
  56. package/src/utils/retitleFontEntries.js +154 -154
  57. package/src/utils/sanitizeForSanityId.js +65 -65
  58. package/src/utils/setupDecompressors.js +27 -27
  59. package/src/utils/updateFontPrices.js +94 -94
  60. package/src/utils/updateTypefaceDocument.js +162 -162
  61. package/src/utils/uploadFontFiles.js +405 -405
  62. package/src/utils/utils.js +24 -24
@@ -1,463 +1,463 @@
1
- // Script-aware uploader for per-script font file variants (Latin, Arabic, Hebrew, etc.) stored in scriptFileInput
2
-
3
- import React, { useState, useEffect, useCallback } from 'react';
4
- import { Stack, Flex, Text, Button } from '@sanity/ui';
5
- import { useFormValue, set, unset } from 'sanity';
6
-
7
- // Utils
8
- import generateCssFile from '../utils/generateCssFile';
9
- import generateFontFile from '../utils/generateFontFile';
10
- import { SCRIPTS } from '../utils/utils';
11
- import { useSanityClient } from '../hooks/useSanityClient';
12
-
13
- /**
14
- * Component for managing font file uploads and conversions for different scripts/languages
15
- * @param {Object} props - Component props
16
- * @param {Object} props.elementProps - Props for the form element
17
- * @param {React.Ref} props.elementProps.ref - Reference for file input
18
- * @param {Function} props.onChange - Callback for handling form value changes
19
- * @param {string} props.value - Current form value
20
- */
21
- export const FontScriptUploaderComponent = (props) => {
22
- const client = useSanityClient();
23
- const {
24
- elementProps: { ref },
25
- onChange,
26
- value = ''
27
- } = props;
28
-
29
- // State management
30
- const [expanded, setExpanded] = useState(SCRIPTS.reduce((acc, language) => ({ ...acc, [language]: true }), {}));
31
- const [message, setMessage] = useState({});
32
- const [status, setStatus] = useState('ready');
33
- const [filenames, setFilenames] = useState({});
34
-
35
- // Form values from Sanity
36
- let scriptFileInput = useFormValue(['scriptFileInput']) || [];
37
- let fileInput = useFormValue(['fileInput']);
38
- let doc_id = useFormValue(['_id']);
39
- let doc_title = useFormValue(['title']);
40
- let doc_variableFont = useFormValue(['variableFont']);
41
- let doc_weight = useFormValue(['weight']);
42
- let doc_style = useFormValue(['style']);
43
- let doc_slug = useFormValue(['slug']);
44
-
45
- /**
46
- * Updates filenames state based on scriptFileInput changes
47
- */
48
- useEffect(() => {
49
- if (!scriptFileInput || Object.keys(scriptFileInput).length === 0) return;
50
- handleSetFilenames();
51
- }, [scriptFileInput]);
52
-
53
- /**
54
- * Fetches and sets filenames for all uploaded font files
55
- */
56
- const handleSetFilenames = useCallback(async () => {
57
- console.log('Set font names ', scriptFileInput);
58
- let allIds = [];
59
-
60
- const assetIds = SCRIPTS.reduce((acc, language) => {
61
- if (scriptFileInput[language]) {
62
- let newFileInput = Object.keys(scriptFileInput[language]).reduce((ftacc, filetype) => {
63
- if (!scriptFileInput[language][filetype]?.asset?._ref) return ftacc;
64
- allIds.push(scriptFileInput[language][filetype]?.asset?._ref);
65
- return { ...ftacc, [filetype]: scriptFileInput[language][filetype]?.asset?._ref }
66
- }, {});
67
- acc[language] = newFileInput;
68
- }
69
- return acc;
70
- }, {});
71
-
72
- // Fetch all assets in a single request
73
- let assetData = await client.fetch(`*[_id in $allIds] {
74
- _id,
75
- originalFilename
76
- }`, { allIds });
77
-
78
- assetData = assetData.reduce((acc, asset) => {
79
- let ref = asset._id;
80
- return { ...acc, [ref]: asset.originalFilename }
81
- }, {});
82
-
83
- let fontNames = {};
84
- SCRIPTS.forEach(language => {
85
- if (assetIds[language]) {
86
- Object.keys(assetIds[language]).forEach(filetype => {
87
- let ref = assetIds[language][filetype];
88
- fontNames[language] = { ...fontNames[language], [filetype]: assetData[ref] }
89
- });
90
- }
91
- });
92
-
93
- setFilenames(fontNames);
94
- }, [scriptFileInput]);
95
-
96
- /**
97
- * Generates CSS file for a specific language
98
- */
99
- const handleGenerateCssFile = useCallback(async (language) => {
100
- setMessage({ ...message, [language]: 'Generating css: ' + doc_title + '.css' });
101
-
102
- const woff2AssetRef = scriptFileInput[language]?.woff2?.asset?._ref;
103
- // Parameterized — prevents injection via scriptFileInput asset refs
104
- let [woff2Buffer] = await client.fetch(
105
- `*[_id == $id]{ originalFilename, url }`,
106
- { id: woff2AssetRef }
107
- );
108
-
109
- let blob = await fetch(woff2Buffer.url);
110
- blob = await blob.blob();
111
-
112
- let newFileInput = await generateCssFile({
113
- woff2File: blob,
114
- fileInput: scriptFileInput,
115
- language: language,
116
- fontName: doc_title,
117
- fileName: woff2Buffer.originalFilename.replace('.woff2', ''),
118
- variableFont: doc_variableFont,
119
- weight: doc_weight,
120
- style: doc_style,
121
- client: client
122
- });
123
-
124
- setMessage({ ...message, [language]: 'CSS generated!' });
125
- setTimeout(() => { setMessage({}) }, 2000);
126
- onChange(set(newFileInput));
127
- }, [scriptFileInput, onChange, doc_title, doc_variableFont]);
128
-
129
- /**
130
- * Generates font files in specified formats
131
- */
132
- const handleGenerateFontFile = useCallback(async (code, sourceFile, language) => {
133
- setMessage({ ...message, [language]: 'Generating files: ', code });
134
-
135
- let url = `https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${sourceFile?.asset?._ref.replace("file-", "").replace("-", ".")}`;
136
- console.log('Handle generate font file ', code, sourceFile, url);
137
-
138
- if (code === 'all') {
139
- await generateFontFile({
140
- codes: ['otf', 'woff', 'woff2', 'eot', 'svg'],
141
- language: language,
142
- srcUrl: url,
143
- filename: doc_slug.current + '-' + language,
144
- documentId: doc_id,
145
- documentTitle: doc_title,
146
- documentVariableFont: doc_variableFont,
147
- documentStyle: doc_style,
148
- documentWeight: doc_weight,
149
- fileInput: scriptFileInput,
150
- });
151
- } else {
152
- await generateFontFile({
153
- codes: [code],
154
- language: language,
155
- srcUrl: url,
156
- filename: doc_slug.current + '-' + language,
157
- documentId: doc_id,
158
- documentTitle: doc_title,
159
- documentVariableFont: doc_variableFont,
160
- documentStyle: doc_style,
161
- documentWeight: doc_weight,
162
- fileInput: scriptFileInput,
163
- });
164
- }
165
- setMessage({ ...message, [language]: 'Files generated!' });
166
- setTimeout(() => { setMessage({}) }, 2000);
167
- }, []);
168
-
169
- /**
170
- * Handles font file upload for a specific language and format
171
- */
172
- const handleUpload = useCallback(async (event, language, code) => {
173
- console.log('Handle upload ', scriptFileInput, language, code);
174
-
175
- let file = event.target.files[0];
176
- let filename = doc_slug.current + '-' + language + '.' + file.name.split('.').pop();
177
-
178
- setMessage({ ...message, [language]: 'Uploading: ' + filename });
179
-
180
- var asset = await client.assets.upload('file', file, { filename: filename });
181
-
182
- let langObj = scriptFileInput[language] ? { ...scriptFileInput[language] } : {};
183
- let newFileInput = {
184
- ...scriptFileInput,
185
- [language]: {
186
- ...langObj,
187
- [code]: {
188
- _type: 'file',
189
- asset: {
190
- _ref: asset._id,
191
- _type: 'reference'
192
- }
193
- }
194
- }
195
- };
196
-
197
- let id = doc_id;
198
- if (id.startsWith('drafts.')) {
199
- id = id.replace('drafts.', '');
200
- }
201
-
202
- setMessage({ ...message, [language]: filename + ' uploaded!' });
203
- setTimeout(() => { setMessage({}) }, 2000);
204
-
205
- // Generate CSS for WOFF2 files
206
- if (code === 'woff2') {
207
- console.log('woff2');
208
- setMessage({ ...message, [language]: 'Generating Css: ' + doc_title + '.css' });
209
-
210
- newFileInput = await generateCssFile({
211
- woff2File: file,
212
- fileInput: newFileInput,
213
- language: language,
214
- fontName: doc_title + '-' + language,
215
- fileName: filename.replace('.woff2', ''),
216
- variableFont: doc_variableFont,
217
- weight: doc_weight,
218
- style: doc_style,
219
- client: client
220
- });
221
- setMessage({ ...message, [language]: '' + doc_title + '.css generated!' });
222
- }
223
-
224
- onChange(set(newFileInput));
225
- }, [scriptFileInput, onChange, doc_title, doc_variableFont, doc_slug]);
226
-
227
- /**
228
- * Deletes a specific font file
229
- */
230
- const handleDelete = useCallback(async (code, language) => {
231
- console.log('Delete : ', code, language);
232
-
233
- setMessage({ ...message, [language]: `deleting ${language} ${code}` });
234
- const asset = scriptFileInput[language][code]?.asset?._ref;
235
-
236
- let newFileInput = { ...scriptFileInput };
237
- delete newFileInput[language][code];
238
-
239
- onChange(unset([language, code]));
240
-
241
- await client.delete(asset)
242
- .then(result => {
243
- setMessage({ ...message, [language]: 'deleted asset: ', result });
244
- setTimeout(() => { setMessage({}) }, 2000);
245
- })
246
- .catch(e => {
247
- console.error('Error deleting asset: ', e.message);
248
- setMessage({ ...message, [language]: 'WARNING: ' + e.message });
249
- });
250
- }, [doc_id, scriptFileInput, onChange]);
251
-
252
- /**
253
- * Deletes all font files for a specific language
254
- */
255
- const handleDeleteAll = useCallback(async (language) => {
256
- setMessage({ ...message, [language]: 'deleting...' });
257
- onChange(unset([language]));
258
-
259
- console.log('Delete all : ', scriptFileInput[language]);
260
- for (var i = 0; i < Object.keys(scriptFileInput[language]).length; i++) {
261
- let refKey = Object.keys(scriptFileInput[language])[i];
262
- if (refKey == 'documentInfo') return;
263
-
264
- const asset = scriptFileInput[language][refKey]?.asset?._ref;
265
-
266
- try {
267
- await client.delete(asset)
268
- .then(result => {
269
- setMessage({ ...message, [language]: 'deleted asset: ', result });
270
- setTimeout(() => { setMessage({}) }, 2000);
271
- });
272
- }
273
- catch (e) {
274
- console.error('Error deleting asset: ', e.message);
275
- }
276
- }
277
- }, [scriptFileInput]);
278
-
279
- // Render component
280
- return (
281
- <Stack space={4}>
282
- {SCRIPTS && scriptFileInput && SCRIPTS.map((language, i) => {
283
- return (
284
- <Stack space={2} key={'language-' + i} style={{ borderBottom: '1px solid var(--card-border-color)', paddingBottom: 8 }}>
285
- <Flex gap={2}>
286
- <Text weight="semibold">{language[0]?.toUpperCase() + language.slice(1)}</Text>
287
- {message && message[language] && message[language] !== '' && (
288
- <Text style={{ color: 'green' }}>{message[language]}</Text>
289
- )}
290
- </Flex>
291
-
292
- {expanded[language] && (
293
- <Stack space={2}>
294
- {/* TTF Section */}
295
- <Flex justify="space-between" align="center">
296
- <Text>
297
- TTF:&nbsp;{!scriptFileInput[language]?.ttf?.asset?._ref
298
- ? (filenames[language]?.ttf ? <b>{filenames[language].ttf}</b> : <b>Empty</b>)
299
- : <a href={`https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${scriptFileInput[language]?.ttf?.asset?._ref.replace("file-", "").replace("-", ".")}`} target="_blank">
300
- {filenames[language]?.ttf ? <b>{filenames[language].ttf}</b> : <b>File</b>}
301
- </a>
302
- }
303
- </Text>
304
- {status === 'ready' && (
305
- <Flex gap={1}>
306
- <label>
307
- <Button as="span" mode="ghost">Upload</Button>
308
- <input ref={ref} type="file" style={{ display: 'none' }} onChange={(event) => handleUpload(event, language, 'ttf')} />
309
- </label>
310
- {value[language]?.ttf && <Button mode="ghost" tone="critical" onClick={() => handleDelete('ttf', language)}>×</Button>}
311
- </Flex>
312
- )}
313
- </Flex>
314
-
315
- {/* Generate All Button */}
316
- {status === 'ready' && value[language]?.ttf && (
317
- <Button mode="default" onClick={() => handleGenerateFontFile('all', value[language].ttf, language)}>
318
- Regenerate Files from TTF
319
- </Button>
320
- )}
321
-
322
- {/* OTF Section */}
323
- <Flex justify="space-between" align="center">
324
- <Text>
325
- OTF:&nbsp;{!scriptFileInput[language]?.otf?.asset?._ref
326
- ? (filenames[language]?.otf ? <b>{filenames[language]?.otf}</b> : <b>Empty</b>)
327
- : <a href={`https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${scriptFileInput[language]?.otf?.asset?._ref.replace("file-", "").replace("-", ".")}`} target="_blank">
328
- {filenames[language]?.otf ? <b>{filenames[language]?.otf}</b> : <b>File</b>}
329
- </a>
330
- }
331
- </Text>
332
- {status === 'ready' && (
333
- <Flex gap={1}>
334
- {value[language]?.woff && <Button mode="default" onClick={() => handleGenerateFontFile('otf', value[language].woff, language)}>Build</Button>}
335
- <label>
336
- <Button as="span" mode="ghost">Upload</Button>
337
- <input ref={ref} type="file" style={{ display: 'none' }} onChange={async (event) => handleUpload(event, language, 'otf')} />
338
- </label>
339
- {value[language]?.otf && <Button mode="ghost" tone="critical" onClick={() => handleDelete('otf', language)}>×</Button>}
340
- </Flex>
341
- )}
342
- </Flex>
343
-
344
- {/* WOFF Section */}
345
- <Flex justify="space-between" align="center">
346
- <Text>
347
- WOFF:&nbsp;{!scriptFileInput[language]?.woff?.asset?._ref
348
- ? (filenames[language]?.woff ? <b>{filenames[language]?.woff}</b> : <b>Empty</b>)
349
- : <a href={`https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${scriptFileInput[language]?.woff?.asset?._ref.replace("file-", "").replace("-", ".")}`} target="_blank">
350
- {filenames[language]?.woff ? <b>{filenames[language]?.woff}</b> : <b>File</b>}
351
- </a>
352
- }
353
- </Text>
354
- {status === 'ready' && (
355
- <Flex gap={1}>
356
- {value[language]?.ttf && <Button mode="default" onClick={() => handleGenerateFontFile('woff', value[language].ttf, language)}>Build</Button>}
357
- <label>
358
- <Button as="span" mode="ghost">Upload</Button>
359
- <input ref={ref} type="file" style={{ display: 'none' }} onChange={async (event) => handleUpload(event, language, 'woff')} />
360
- </label>
361
- {value[language]?.woff && <Button mode="ghost" tone="critical" onClick={() => handleDelete('woff', language)}>×</Button>}
362
- </Flex>
363
- )}
364
- </Flex>
365
-
366
- {/* WOFF2 Section */}
367
- <Flex justify="space-between" align="center">
368
- <Text>
369
- WOFF2:&nbsp;{!scriptFileInput[language]?.woff2?.asset?._ref
370
- ? (filenames[language]?.woff2 ? <b>{filenames[language]?.woff2}</b> : <b>Empty</b>)
371
- : <a href={`https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${scriptFileInput[language]?.woff2?.asset?._ref.replace("file-", "").replace("-", ".")}`} target="_blank">
372
- {filenames[language]?.woff2 ? <b>{filenames[language]?.woff2}</b> : <b>File</b>}
373
- </a>
374
- }
375
- </Text>
376
- {status === 'ready' && (
377
- <Flex gap={1}>
378
- {value[language]?.ttf && <Button mode="default" onClick={() => handleGenerateFontFile('woff2', value[language].ttf, language)}>Build</Button>}
379
- <label>
380
- <Button as="span" mode="ghost">Upload</Button>
381
- <input ref={ref} type="file" style={{ display: 'none' }} onChange={async (event) => handleUpload(event, language, 'woff2')} />
382
- </label>
383
- {value[language]?.woff2 && <Button mode="ghost" tone="critical" onClick={() => handleDelete('woff2', language)}>×</Button>}
384
- </Flex>
385
- )}
386
- </Flex>
387
-
388
- {/* EOT Section */}
389
- <Flex justify="space-between" align="center">
390
- <Text>
391
- EOT:&nbsp;{!scriptFileInput[language]?.eot?.asset?._ref
392
- ? (filenames[language]?.eot ? <b>{filenames[language]?.eot}</b> : <b>Empty</b>)
393
- : <a href={`https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${scriptFileInput[language]?.eot?.asset?._ref.replace("file-", "").replace("-", ".")}`} target="_blank">
394
- {filenames[language]?.eot ? <b>{filenames[language]?.eot}</b> : <b>File</b>}
395
- </a>
396
- }
397
- </Text>
398
- {status === 'ready' && (
399
- <Flex gap={1}>
400
- {value[language]?.ttf && <Button mode="default" onClick={() => handleGenerateFontFile('eot', value[language].ttf, language)}>Build</Button>}
401
- <label>
402
- <Button as="span" mode="ghost">Upload</Button>
403
- <input ref={ref} type="file" style={{ display: 'none' }} onChange={async (event) => handleUpload(event, language, 'eot')} />
404
- </label>
405
- {value[language]?.eot && <Button mode="ghost" tone="critical" onClick={() => handleDelete('eot', language)}>×</Button>}
406
- </Flex>
407
- )}
408
- </Flex>
409
-
410
- {/* SVG Section */}
411
- <Flex justify="space-between" align="center">
412
- <Text>
413
- SVG:&nbsp;{!scriptFileInput[language]?.svg?.asset?._ref
414
- ? (filenames[language]?.svg ? <b>{filenames[language]?.svg}</b> : <b>Empty</b>)
415
- : <a href={`https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${scriptFileInput[language]?.svg?.asset?._ref.replace("file-", "").replace("-", ".")}`} target="_blank">
416
- {filenames[language]?.svg ? <b>{filenames[language]?.svg}</b> : <b>File</b>}
417
- </a>
418
- }
419
- </Text>
420
- {status === 'ready' && (
421
- <Flex gap={1}>
422
- {value[language]?.ttf && <Button mode="default" onClick={() => handleGenerateFontFile('svg', value[language].ttf, language)}>Build</Button>}
423
- <label>
424
- <Button as="span" mode="ghost">Upload</Button>
425
- <input ref={ref} type="file" style={{ display: 'none' }} onChange={async (event) => handleUpload(event, language, 'svg')} />
426
- </label>
427
- {value[language]?.svg && <Button mode="ghost" tone="critical" onClick={() => handleDelete('svg', language)}>×</Button>}
428
- </Flex>
429
- )}
430
- </Flex>
431
-
432
- {/* CSS Section */}
433
- <Flex justify="space-between" align="center">
434
- <Text>
435
- CSS:&nbsp;{!scriptFileInput[language]?.css?.asset?._ref
436
- ? (filenames[language]?.css ? <b>{filenames[language]?.css}</b> : <b>Empty</b>)
437
- : <a href={`https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${scriptFileInput[language]?.css?.asset?._ref.replace("file-", "").replace("-", ".")}`} target="_blank">
438
- {filenames[language]?.css ? <b>{filenames[language]?.css}</b> : <b>File</b>}
439
- </a>
440
- }
441
- </Text>
442
- {status === 'ready' && (
443
- <Flex gap={1}>
444
- {value[language]?.woff2 && <Button mode="default" onClick={() => handleGenerateCssFile(language)}>Build</Button>}
445
- {value[language]?.css && <Button mode="ghost" tone="critical" onClick={() => handleDelete('css', language)}>×</Button>}
446
- </Flex>
447
- )}
448
- </Flex>
449
-
450
- {/* Delete All Button */}
451
- {status === 'ready' && (value[language]?.ttf || value[language]?.otf || value[language]?.woff || value[language]?.woff2) && (
452
- <Button mode="ghost" tone="critical" onClick={() => handleDeleteAll(language)} style={{ width: '100%' }}>
453
- Delete All
454
- </Button>
455
- )}
456
- </Stack>
457
- )}
458
- </Stack>
459
- )
460
- })}
461
- </Stack>
462
- )
463
- }
1
+ // Script-aware uploader for per-script font file variants (Latin, Arabic, Hebrew, etc.) stored in scriptFileInput
2
+
3
+ import React, { useState, useEffect, useCallback } from 'react';
4
+ import { Stack, Flex, Text, Button } from '@sanity/ui';
5
+ import { useFormValue, set, unset } from 'sanity';
6
+
7
+ // Utils
8
+ import generateCssFile from '../utils/generateCssFile';
9
+ import generateFontFile from '../utils/generateFontFile';
10
+ import { SCRIPTS } from '../utils/utils';
11
+ import { useSanityClient } from '../hooks/useSanityClient';
12
+
13
+ /**
14
+ * Component for managing font file uploads and conversions for different scripts/languages
15
+ * @param {Object} props - Component props
16
+ * @param {Object} props.elementProps - Props for the form element
17
+ * @param {React.Ref} props.elementProps.ref - Reference for file input
18
+ * @param {Function} props.onChange - Callback for handling form value changes
19
+ * @param {string} props.value - Current form value
20
+ */
21
+ export const FontScriptUploaderComponent = (props) => {
22
+ const client = useSanityClient();
23
+ const {
24
+ elementProps: { ref },
25
+ onChange,
26
+ value = ''
27
+ } = props;
28
+
29
+ // State management
30
+ const [expanded, setExpanded] = useState(SCRIPTS.reduce((acc, language) => ({ ...acc, [language]: true }), {}));
31
+ const [message, setMessage] = useState({});
32
+ const [status, setStatus] = useState('ready');
33
+ const [filenames, setFilenames] = useState({});
34
+
35
+ // Form values from Sanity
36
+ let scriptFileInput = useFormValue(['scriptFileInput']) || [];
37
+ let fileInput = useFormValue(['fileInput']);
38
+ let doc_id = useFormValue(['_id']);
39
+ let doc_title = useFormValue(['title']);
40
+ let doc_variableFont = useFormValue(['variableFont']);
41
+ let doc_weight = useFormValue(['weight']);
42
+ let doc_style = useFormValue(['style']);
43
+ let doc_slug = useFormValue(['slug']);
44
+
45
+ /**
46
+ * Updates filenames state based on scriptFileInput changes
47
+ */
48
+ useEffect(() => {
49
+ if (!scriptFileInput || Object.keys(scriptFileInput).length === 0) return;
50
+ handleSetFilenames();
51
+ }, [scriptFileInput]);
52
+
53
+ /**
54
+ * Fetches and sets filenames for all uploaded font files
55
+ */
56
+ const handleSetFilenames = useCallback(async () => {
57
+ console.log('Set font names ', scriptFileInput);
58
+ let allIds = [];
59
+
60
+ const assetIds = SCRIPTS.reduce((acc, language) => {
61
+ if (scriptFileInput[language]) {
62
+ let newFileInput = Object.keys(scriptFileInput[language]).reduce((ftacc, filetype) => {
63
+ if (!scriptFileInput[language][filetype]?.asset?._ref) return ftacc;
64
+ allIds.push(scriptFileInput[language][filetype]?.asset?._ref);
65
+ return { ...ftacc, [filetype]: scriptFileInput[language][filetype]?.asset?._ref }
66
+ }, {});
67
+ acc[language] = newFileInput;
68
+ }
69
+ return acc;
70
+ }, {});
71
+
72
+ // Fetch all assets in a single request
73
+ let assetData = await client.fetch(`*[_id in $allIds] {
74
+ _id,
75
+ originalFilename
76
+ }`, { allIds });
77
+
78
+ assetData = assetData.reduce((acc, asset) => {
79
+ let ref = asset._id;
80
+ return { ...acc, [ref]: asset.originalFilename }
81
+ }, {});
82
+
83
+ let fontNames = {};
84
+ SCRIPTS.forEach(language => {
85
+ if (assetIds[language]) {
86
+ Object.keys(assetIds[language]).forEach(filetype => {
87
+ let ref = assetIds[language][filetype];
88
+ fontNames[language] = { ...fontNames[language], [filetype]: assetData[ref] }
89
+ });
90
+ }
91
+ });
92
+
93
+ setFilenames(fontNames);
94
+ }, [scriptFileInput]);
95
+
96
+ /**
97
+ * Generates CSS file for a specific language
98
+ */
99
+ const handleGenerateCssFile = useCallback(async (language) => {
100
+ setMessage({ ...message, [language]: 'Generating css: ' + doc_title + '.css' });
101
+
102
+ const woff2AssetRef = scriptFileInput[language]?.woff2?.asset?._ref;
103
+ // Parameterized — prevents injection via scriptFileInput asset refs
104
+ let [woff2Buffer] = await client.fetch(
105
+ `*[_id == $id]{ originalFilename, url }`,
106
+ { id: woff2AssetRef }
107
+ );
108
+
109
+ let blob = await fetch(woff2Buffer.url);
110
+ blob = await blob.blob();
111
+
112
+ let newFileInput = await generateCssFile({
113
+ woff2File: blob,
114
+ fileInput: scriptFileInput,
115
+ language: language,
116
+ fontName: doc_title,
117
+ fileName: woff2Buffer.originalFilename.replace('.woff2', ''),
118
+ variableFont: doc_variableFont,
119
+ weight: doc_weight,
120
+ style: doc_style,
121
+ client: client
122
+ });
123
+
124
+ setMessage({ ...message, [language]: 'CSS generated!' });
125
+ setTimeout(() => { setMessage({}) }, 2000);
126
+ onChange(set(newFileInput));
127
+ }, [scriptFileInput, onChange, doc_title, doc_variableFont]);
128
+
129
+ /**
130
+ * Generates font files in specified formats
131
+ */
132
+ const handleGenerateFontFile = useCallback(async (code, sourceFile, language) => {
133
+ setMessage({ ...message, [language]: 'Generating files: ', code });
134
+
135
+ let url = `https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${sourceFile?.asset?._ref.replace("file-", "").replace("-", ".")}`;
136
+ console.log('Handle generate font file ', code, sourceFile, url);
137
+
138
+ if (code === 'all') {
139
+ await generateFontFile({
140
+ codes: ['otf', 'woff', 'woff2', 'eot', 'svg'],
141
+ language: language,
142
+ srcUrl: url,
143
+ filename: doc_slug.current + '-' + language,
144
+ documentId: doc_id,
145
+ documentTitle: doc_title,
146
+ documentVariableFont: doc_variableFont,
147
+ documentStyle: doc_style,
148
+ documentWeight: doc_weight,
149
+ fileInput: scriptFileInput,
150
+ });
151
+ } else {
152
+ await generateFontFile({
153
+ codes: [code],
154
+ language: language,
155
+ srcUrl: url,
156
+ filename: doc_slug.current + '-' + language,
157
+ documentId: doc_id,
158
+ documentTitle: doc_title,
159
+ documentVariableFont: doc_variableFont,
160
+ documentStyle: doc_style,
161
+ documentWeight: doc_weight,
162
+ fileInput: scriptFileInput,
163
+ });
164
+ }
165
+ setMessage({ ...message, [language]: 'Files generated!' });
166
+ setTimeout(() => { setMessage({}) }, 2000);
167
+ }, []);
168
+
169
+ /**
170
+ * Handles font file upload for a specific language and format
171
+ */
172
+ const handleUpload = useCallback(async (event, language, code) => {
173
+ console.log('Handle upload ', scriptFileInput, language, code);
174
+
175
+ let file = event.target.files[0];
176
+ let filename = doc_slug.current + '-' + language + '.' + file.name.split('.').pop();
177
+
178
+ setMessage({ ...message, [language]: 'Uploading: ' + filename });
179
+
180
+ var asset = await client.assets.upload('file', file, { filename: filename });
181
+
182
+ let langObj = scriptFileInput[language] ? { ...scriptFileInput[language] } : {};
183
+ let newFileInput = {
184
+ ...scriptFileInput,
185
+ [language]: {
186
+ ...langObj,
187
+ [code]: {
188
+ _type: 'file',
189
+ asset: {
190
+ _ref: asset._id,
191
+ _type: 'reference'
192
+ }
193
+ }
194
+ }
195
+ };
196
+
197
+ let id = doc_id;
198
+ if (id.startsWith('drafts.')) {
199
+ id = id.replace('drafts.', '');
200
+ }
201
+
202
+ setMessage({ ...message, [language]: filename + ' uploaded!' });
203
+ setTimeout(() => { setMessage({}) }, 2000);
204
+
205
+ // Generate CSS for WOFF2 files
206
+ if (code === 'woff2') {
207
+ console.log('woff2');
208
+ setMessage({ ...message, [language]: 'Generating Css: ' + doc_title + '.css' });
209
+
210
+ newFileInput = await generateCssFile({
211
+ woff2File: file,
212
+ fileInput: newFileInput,
213
+ language: language,
214
+ fontName: doc_title + '-' + language,
215
+ fileName: filename.replace('.woff2', ''),
216
+ variableFont: doc_variableFont,
217
+ weight: doc_weight,
218
+ style: doc_style,
219
+ client: client
220
+ });
221
+ setMessage({ ...message, [language]: '' + doc_title + '.css generated!' });
222
+ }
223
+
224
+ onChange(set(newFileInput));
225
+ }, [scriptFileInput, onChange, doc_title, doc_variableFont, doc_slug]);
226
+
227
+ /**
228
+ * Deletes a specific font file
229
+ */
230
+ const handleDelete = useCallback(async (code, language) => {
231
+ console.log('Delete : ', code, language);
232
+
233
+ setMessage({ ...message, [language]: `deleting ${language} ${code}` });
234
+ const asset = scriptFileInput[language][code]?.asset?._ref;
235
+
236
+ let newFileInput = { ...scriptFileInput };
237
+ delete newFileInput[language][code];
238
+
239
+ onChange(unset([language, code]));
240
+
241
+ await client.delete(asset)
242
+ .then(result => {
243
+ setMessage({ ...message, [language]: 'deleted asset: ', result });
244
+ setTimeout(() => { setMessage({}) }, 2000);
245
+ })
246
+ .catch(e => {
247
+ console.error('Error deleting asset: ', e.message);
248
+ setMessage({ ...message, [language]: 'WARNING: ' + e.message });
249
+ });
250
+ }, [doc_id, scriptFileInput, onChange]);
251
+
252
+ /**
253
+ * Deletes all font files for a specific language
254
+ */
255
+ const handleDeleteAll = useCallback(async (language) => {
256
+ setMessage({ ...message, [language]: 'deleting...' });
257
+ onChange(unset([language]));
258
+
259
+ console.log('Delete all : ', scriptFileInput[language]);
260
+ for (var i = 0; i < Object.keys(scriptFileInput[language]).length; i++) {
261
+ let refKey = Object.keys(scriptFileInput[language])[i];
262
+ if (refKey == 'documentInfo') return;
263
+
264
+ const asset = scriptFileInput[language][refKey]?.asset?._ref;
265
+
266
+ try {
267
+ await client.delete(asset)
268
+ .then(result => {
269
+ setMessage({ ...message, [language]: 'deleted asset: ', result });
270
+ setTimeout(() => { setMessage({}) }, 2000);
271
+ });
272
+ }
273
+ catch (e) {
274
+ console.error('Error deleting asset: ', e.message);
275
+ }
276
+ }
277
+ }, [scriptFileInput]);
278
+
279
+ // Render component
280
+ return (
281
+ <Stack space={4}>
282
+ {SCRIPTS && scriptFileInput && SCRIPTS.map((language, i) => {
283
+ return (
284
+ <Stack space={2} key={'language-' + i} style={{ borderBottom: '1px solid var(--card-border-color)', paddingBottom: 8 }}>
285
+ <Flex gap={2}>
286
+ <Text weight="semibold">{language[0]?.toUpperCase() + language.slice(1)}</Text>
287
+ {message && message[language] && message[language] !== '' && (
288
+ <Text style={{ color: 'green' }}>{message[language]}</Text>
289
+ )}
290
+ </Flex>
291
+
292
+ {expanded[language] && (
293
+ <Stack space={2}>
294
+ {/* TTF Section */}
295
+ <Flex justify="space-between" align="center">
296
+ <Text>
297
+ TTF:&nbsp;{!scriptFileInput[language]?.ttf?.asset?._ref
298
+ ? (filenames[language]?.ttf ? <b>{filenames[language].ttf}</b> : <b>Empty</b>)
299
+ : <a href={`https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${scriptFileInput[language]?.ttf?.asset?._ref.replace("file-", "").replace("-", ".")}`} target="_blank">
300
+ {filenames[language]?.ttf ? <b>{filenames[language].ttf}</b> : <b>File</b>}
301
+ </a>
302
+ }
303
+ </Text>
304
+ {status === 'ready' && (
305
+ <Flex gap={1}>
306
+ <label>
307
+ <Button as="span" mode="ghost">Upload</Button>
308
+ <input ref={ref} type="file" style={{ display: 'none' }} onChange={(event) => handleUpload(event, language, 'ttf')} />
309
+ </label>
310
+ {value[language]?.ttf && <Button mode="ghost" tone="critical" onClick={() => handleDelete('ttf', language)}>×</Button>}
311
+ </Flex>
312
+ )}
313
+ </Flex>
314
+
315
+ {/* Generate All Button */}
316
+ {status === 'ready' && value[language]?.ttf && (
317
+ <Button mode="default" onClick={() => handleGenerateFontFile('all', value[language].ttf, language)}>
318
+ Regenerate Files from TTF
319
+ </Button>
320
+ )}
321
+
322
+ {/* OTF Section */}
323
+ <Flex justify="space-between" align="center">
324
+ <Text>
325
+ OTF:&nbsp;{!scriptFileInput[language]?.otf?.asset?._ref
326
+ ? (filenames[language]?.otf ? <b>{filenames[language]?.otf}</b> : <b>Empty</b>)
327
+ : <a href={`https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${scriptFileInput[language]?.otf?.asset?._ref.replace("file-", "").replace("-", ".")}`} target="_blank">
328
+ {filenames[language]?.otf ? <b>{filenames[language]?.otf}</b> : <b>File</b>}
329
+ </a>
330
+ }
331
+ </Text>
332
+ {status === 'ready' && (
333
+ <Flex gap={1}>
334
+ {value[language]?.woff && <Button mode="default" onClick={() => handleGenerateFontFile('otf', value[language].woff, language)}>Build</Button>}
335
+ <label>
336
+ <Button as="span" mode="ghost">Upload</Button>
337
+ <input ref={ref} type="file" style={{ display: 'none' }} onChange={async (event) => handleUpload(event, language, 'otf')} />
338
+ </label>
339
+ {value[language]?.otf && <Button mode="ghost" tone="critical" onClick={() => handleDelete('otf', language)}>×</Button>}
340
+ </Flex>
341
+ )}
342
+ </Flex>
343
+
344
+ {/* WOFF Section */}
345
+ <Flex justify="space-between" align="center">
346
+ <Text>
347
+ WOFF:&nbsp;{!scriptFileInput[language]?.woff?.asset?._ref
348
+ ? (filenames[language]?.woff ? <b>{filenames[language]?.woff}</b> : <b>Empty</b>)
349
+ : <a href={`https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${scriptFileInput[language]?.woff?.asset?._ref.replace("file-", "").replace("-", ".")}`} target="_blank">
350
+ {filenames[language]?.woff ? <b>{filenames[language]?.woff}</b> : <b>File</b>}
351
+ </a>
352
+ }
353
+ </Text>
354
+ {status === 'ready' && (
355
+ <Flex gap={1}>
356
+ {value[language]?.ttf && <Button mode="default" onClick={() => handleGenerateFontFile('woff', value[language].ttf, language)}>Build</Button>}
357
+ <label>
358
+ <Button as="span" mode="ghost">Upload</Button>
359
+ <input ref={ref} type="file" style={{ display: 'none' }} onChange={async (event) => handleUpload(event, language, 'woff')} />
360
+ </label>
361
+ {value[language]?.woff && <Button mode="ghost" tone="critical" onClick={() => handleDelete('woff', language)}>×</Button>}
362
+ </Flex>
363
+ )}
364
+ </Flex>
365
+
366
+ {/* WOFF2 Section */}
367
+ <Flex justify="space-between" align="center">
368
+ <Text>
369
+ WOFF2:&nbsp;{!scriptFileInput[language]?.woff2?.asset?._ref
370
+ ? (filenames[language]?.woff2 ? <b>{filenames[language]?.woff2}</b> : <b>Empty</b>)
371
+ : <a href={`https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${scriptFileInput[language]?.woff2?.asset?._ref.replace("file-", "").replace("-", ".")}`} target="_blank">
372
+ {filenames[language]?.woff2 ? <b>{filenames[language]?.woff2}</b> : <b>File</b>}
373
+ </a>
374
+ }
375
+ </Text>
376
+ {status === 'ready' && (
377
+ <Flex gap={1}>
378
+ {value[language]?.ttf && <Button mode="default" onClick={() => handleGenerateFontFile('woff2', value[language].ttf, language)}>Build</Button>}
379
+ <label>
380
+ <Button as="span" mode="ghost">Upload</Button>
381
+ <input ref={ref} type="file" style={{ display: 'none' }} onChange={async (event) => handleUpload(event, language, 'woff2')} />
382
+ </label>
383
+ {value[language]?.woff2 && <Button mode="ghost" tone="critical" onClick={() => handleDelete('woff2', language)}>×</Button>}
384
+ </Flex>
385
+ )}
386
+ </Flex>
387
+
388
+ {/* EOT Section */}
389
+ <Flex justify="space-between" align="center">
390
+ <Text>
391
+ EOT:&nbsp;{!scriptFileInput[language]?.eot?.asset?._ref
392
+ ? (filenames[language]?.eot ? <b>{filenames[language]?.eot}</b> : <b>Empty</b>)
393
+ : <a href={`https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${scriptFileInput[language]?.eot?.asset?._ref.replace("file-", "").replace("-", ".")}`} target="_blank">
394
+ {filenames[language]?.eot ? <b>{filenames[language]?.eot}</b> : <b>File</b>}
395
+ </a>
396
+ }
397
+ </Text>
398
+ {status === 'ready' && (
399
+ <Flex gap={1}>
400
+ {value[language]?.ttf && <Button mode="default" onClick={() => handleGenerateFontFile('eot', value[language].ttf, language)}>Build</Button>}
401
+ <label>
402
+ <Button as="span" mode="ghost">Upload</Button>
403
+ <input ref={ref} type="file" style={{ display: 'none' }} onChange={async (event) => handleUpload(event, language, 'eot')} />
404
+ </label>
405
+ {value[language]?.eot && <Button mode="ghost" tone="critical" onClick={() => handleDelete('eot', language)}>×</Button>}
406
+ </Flex>
407
+ )}
408
+ </Flex>
409
+
410
+ {/* SVG Section */}
411
+ <Flex justify="space-between" align="center">
412
+ <Text>
413
+ SVG:&nbsp;{!scriptFileInput[language]?.svg?.asset?._ref
414
+ ? (filenames[language]?.svg ? <b>{filenames[language]?.svg}</b> : <b>Empty</b>)
415
+ : <a href={`https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${scriptFileInput[language]?.svg?.asset?._ref.replace("file-", "").replace("-", ".")}`} target="_blank">
416
+ {filenames[language]?.svg ? <b>{filenames[language]?.svg}</b> : <b>File</b>}
417
+ </a>
418
+ }
419
+ </Text>
420
+ {status === 'ready' && (
421
+ <Flex gap={1}>
422
+ {value[language]?.ttf && <Button mode="default" onClick={() => handleGenerateFontFile('svg', value[language].ttf, language)}>Build</Button>}
423
+ <label>
424
+ <Button as="span" mode="ghost">Upload</Button>
425
+ <input ref={ref} type="file" style={{ display: 'none' }} onChange={async (event) => handleUpload(event, language, 'svg')} />
426
+ </label>
427
+ {value[language]?.svg && <Button mode="ghost" tone="critical" onClick={() => handleDelete('svg', language)}>×</Button>}
428
+ </Flex>
429
+ )}
430
+ </Flex>
431
+
432
+ {/* CSS Section */}
433
+ <Flex justify="space-between" align="center">
434
+ <Text>
435
+ CSS:&nbsp;{!scriptFileInput[language]?.css?.asset?._ref
436
+ ? (filenames[language]?.css ? <b>{filenames[language]?.css}</b> : <b>Empty</b>)
437
+ : <a href={`https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${scriptFileInput[language]?.css?.asset?._ref.replace("file-", "").replace("-", ".")}`} target="_blank">
438
+ {filenames[language]?.css ? <b>{filenames[language]?.css}</b> : <b>File</b>}
439
+ </a>
440
+ }
441
+ </Text>
442
+ {status === 'ready' && (
443
+ <Flex gap={1}>
444
+ {value[language]?.woff2 && <Button mode="default" onClick={() => handleGenerateCssFile(language)}>Build</Button>}
445
+ {value[language]?.css && <Button mode="ghost" tone="critical" onClick={() => handleDelete('css', language)}>×</Button>}
446
+ </Flex>
447
+ )}
448
+ </Flex>
449
+
450
+ {/* Delete All Button */}
451
+ {status === 'ready' && (value[language]?.ttf || value[language]?.otf || value[language]?.woff || value[language]?.woff2) && (
452
+ <Button mode="ghost" tone="critical" onClick={() => handleDeleteAll(language)} style={{ width: '100%' }}>
453
+ Delete All
454
+ </Button>
455
+ )}
456
+ </Stack>
457
+ )}
458
+ </Stack>
459
+ )
460
+ })}
461
+ </Stack>
462
+ )
463
+ }