@liiift-studio/sanity-font-manager 2.3.19 → 2.5.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/README.md +437 -437
  2. package/dist/UploadModal-6LIX7XOK.js +6 -0
  3. package/dist/UploadModal-NME2W53V.mjs +6 -0
  4. package/dist/chunk-646WCBRR.mjs +7276 -0
  5. package/dist/chunk-FH4QKHOH.js +7276 -0
  6. package/dist/index.js +747 -1675
  7. package/dist/index.mjs +400 -1237
  8. package/package.json +85 -85
  9. package/src/components/BatchUploadFonts.jsx +653 -639
  10. package/src/components/BulkActions.jsx +99 -0
  11. package/src/components/ExistingDocumentResolver.jsx +152 -0
  12. package/src/components/FontReviewCard.jsx +415 -0
  13. package/src/components/FontScriptUploaderComponent.jsx +463 -463
  14. package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
  15. package/src/components/KeyValueInput.jsx +95 -95
  16. package/src/components/KeyValueReferenceInput.jsx +254 -254
  17. package/src/components/NestedObjectArraySelector.jsx +146 -146
  18. package/src/components/PriceInput.jsx +26 -26
  19. package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
  20. package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
  21. package/src/components/SetOTF.jsx +87 -87
  22. package/src/components/SingleUploaderTool.jsx +672 -673
  23. package/src/components/StatusDisplay.jsx +26 -26
  24. package/src/components/StyleCountInput.jsx +16 -16
  25. package/src/components/UpdateScriptsComponent.jsx +76 -76
  26. package/src/components/UploadButton.jsx +43 -43
  27. package/src/components/UploadModal.jsx +268 -0
  28. package/src/components/UploadScriptsComponent.jsx +539 -537
  29. package/src/components/UploadStep1Settings.jsx +272 -0
  30. package/src/components/UploadStep2Review.jsx +472 -0
  31. package/src/components/UploadStep3Execute.jsx +234 -0
  32. package/src/components/UploadSummary.jsx +196 -0
  33. package/src/components/VariableInstanceReferencesInput.jsx +190 -190
  34. package/src/hooks/useNestedObjects.js +92 -92
  35. package/src/hooks/useSanityClient.js +9 -9
  36. package/src/index.js +115 -70
  37. package/src/schema/openTypeField.js +1945 -1945
  38. package/src/schema/styleCountField.js +12 -12
  39. package/src/schema/stylesField.js +268 -268
  40. package/src/schema/stylisticSetField.js +301 -301
  41. package/src/utils/buildUploadPlan.js +325 -0
  42. package/src/utils/executeUploadPlan.js +437 -0
  43. package/src/utils/executionReducer.js +56 -0
  44. package/src/utils/fontHelpers.js +267 -0
  45. package/src/utils/generateCssFile.js +207 -205
  46. package/src/utils/generateFontData.js +98 -145
  47. package/src/utils/generateFontFile.js +38 -38
  48. package/src/utils/generateKeywords.js +185 -185
  49. package/src/utils/generateSubset.js +45 -45
  50. package/src/utils/getEmptyFontKit.js +101 -99
  51. package/src/utils/parseFont.js +55 -0
  52. package/src/utils/parseVariableFontInstances.js +211 -211
  53. package/src/utils/planReducer.js +517 -0
  54. package/src/utils/planTypes.js +183 -0
  55. package/src/utils/processFontFiles.js +529 -477
  56. package/src/utils/regenerateFontData.js +146 -146
  57. package/src/utils/resolveExistingFont.js +87 -0
  58. package/src/utils/sanitizeForSanityId.js +65 -65
  59. package/src/utils/updateFontPrices.js +94 -94
  60. package/src/utils/updateTypefaceDocument.js +149 -160
  61. package/src/utils/uploadFontFiles.js +405 -316
  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
+ }