@inoo-ch/payload-image-optimizer 1.0.0 → 1.1.1
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/AGENT_DOCS.md +383 -0
- package/README.md +18 -1
- package/dist/components/RegenerationButton.js +220 -16
- package/dist/components/RegenerationButton.js.map +1 -1
- package/dist/defaults.js +5 -6
- package/dist/defaults.js.map +1 -1
- package/dist/hooks/afterChange.js +38 -13
- package/dist/hooks/afterChange.js.map +1 -1
- package/dist/hooks/beforeChange.js +21 -4
- package/dist/hooks/beforeChange.js.map +1 -1
- package/dist/tasks/convertFormats.js +4 -1
- package/dist/tasks/convertFormats.js.map +1 -1
- package/dist/tasks/regenerateDocument.js +43 -17
- package/dist/tasks/regenerateDocument.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.js.map +1 -1
- package/package.json +34 -60
- package/src/components/ImageBox.tsx +80 -0
- package/src/components/OptimizationStatus.tsx +137 -0
- package/src/components/RegenerationButton.tsx +356 -0
- package/src/defaults.ts +36 -0
- package/src/endpoints/regenerate.ts +125 -0
- package/src/exports/client.ts +6 -0
- package/src/exports/rsc.ts +1 -0
- package/src/fields/imageOptimizerField.ts +70 -0
- package/src/hooks/afterChange.ts +77 -0
- package/src/hooks/beforeChange.ts +64 -0
- package/src/index.ts +125 -0
- package/src/next-image.d.ts +3 -0
- package/src/processing/index.ts +59 -0
- package/src/tasks/convertFormats.ts +107 -0
- package/src/tasks/regenerateDocument.ts +177 -0
- package/src/types.ts +38 -0
- package/src/utilities/getImageOptimizerProps.ts +58 -0
- package/src/utilities/thumbhash.ts +15 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tasks/convertFormats.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\n\nimport type { CollectionSlug } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { convertFormat } from '../processing/index.js'\n\nexport const createConvertFormatsHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n return async ({ input, req }: { input: { collectionSlug: string; docId: string }; req: any }) => {\n try {\n const doc = await req.payload.findByID({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n })\n\n const collectionConfig = req.payload.collections[input.collectionSlug as keyof typeof req.payload.collections].config\n\n let staticDir: string =\n typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : ''\n if (!staticDir) {\n throw new Error(`No staticDir configured for collection \"${input.collectionSlug}\"`)\n }\n if (!path.isAbsolute(staticDir)) {\n staticDir = path.resolve(process.cwd(), staticDir)\n }\n\n // Sanitize filename to prevent path traversal\n const safeFilename = path.basename(doc.filename)\n const filePath = path.join(staticDir, safeFilename)\n const fileBuffer = await fs.readFile(filePath)\n\n const variants: Array<{\n filename: string\n filesize: number\n format: string\n height: number\n mimeType: string\n url: string\n width: number\n }> = []\n\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug)\n\n for
|
|
1
|
+
{"version":3,"sources":["../../src/tasks/convertFormats.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\n\nimport type { CollectionSlug } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { convertFormat } from '../processing/index.js'\n\nexport const createConvertFormatsHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n return async ({ input, req }: { input: { collectionSlug: string; docId: string }; req: any }) => {\n try {\n const doc = await req.payload.findByID({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n })\n\n const collectionConfig = req.payload.collections[input.collectionSlug as keyof typeof req.payload.collections].config\n\n let staticDir: string =\n typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : ''\n if (!staticDir) {\n throw new Error(`No staticDir configured for collection \"${input.collectionSlug}\"`)\n }\n if (!path.isAbsolute(staticDir)) {\n staticDir = path.resolve(process.cwd(), staticDir)\n }\n\n // Sanitize filename to prevent path traversal\n const safeFilename = path.basename(doc.filename)\n const filePath = path.join(staticDir, safeFilename)\n const fileBuffer = await fs.readFile(filePath)\n\n const variants: Array<{\n filename: string\n filesize: number\n format: string\n height: number\n mimeType: string\n url: string\n width: number\n }> = []\n\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug)\n\n // When replaceOriginal is on, the main file is already in the primary format —\n // skip it and only generate variants for the remaining formats.\n const formatsToGenerate = perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0\n ? perCollectionConfig.formats.slice(1)\n : perCollectionConfig.formats\n\n for (const format of formatsToGenerate) {\n const result = await convertFormat(fileBuffer, format.format, format.quality)\n const variantFilename = `${path.parse(safeFilename).name}-optimized.${format.format}`\n\n await fs.writeFile(path.join(staticDir, variantFilename), result.buffer)\n\n variants.push({\n format: format.format,\n filename: variantFilename,\n filesize: result.size,\n width: result.width,\n height: result.height,\n mimeType: result.mimeType,\n url: `/api/${input.collectionSlug}/file/${variantFilename}`,\n })\n }\n\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n status: 'complete',\n variants,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n\n return { output: { variantsGenerated: variants.length } }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err)\n\n try {\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n status: 'error',\n error: errorMessage,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n } catch (updateErr) {\n req.payload.logger.error(\n { err: updateErr },\n 'Failed to persist error status for image optimizer',\n )\n }\n\n throw err\n }\n }\n}\n"],"names":["fs","path","resolveCollectionConfig","convertFormat","createConvertFormatsHandler","resolvedConfig","input","req","doc","payload","findByID","collection","collectionSlug","id","docId","collectionConfig","collections","config","staticDir","upload","Error","isAbsolute","resolve","process","cwd","safeFilename","basename","filename","filePath","join","fileBuffer","readFile","variants","perCollectionConfig","formatsToGenerate","replaceOriginal","formats","length","slice","format","result","quality","variantFilename","parse","name","writeFile","buffer","push","filesize","size","width","height","mimeType","url","update","data","imageOptimizer","status","context","imageOptimizer_skip","output","variantsGenerated","err","errorMessage","message","String","error","updateErr","logger"],"mappings":"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"}
|
|
@@ -46,18 +46,37 @@ export const createRegenerateDocumentHandler = (resolvedConfig)=>{
|
|
|
46
46
|
const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug);
|
|
47
47
|
// Step 1: Strip metadata + resize
|
|
48
48
|
const processed = await stripAndResize(fileBuffer, perCollectionConfig.maxDimensions, resolvedConfig.stripMetadata);
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
let mainBuffer = processed.buffer;
|
|
50
|
+
let mainSize = processed.size;
|
|
51
|
+
let newFilename = safeFilename;
|
|
52
|
+
let newMimeType;
|
|
53
|
+
// Step 1b: If replaceOriginal, convert main file to primary format
|
|
54
|
+
if (perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0) {
|
|
55
|
+
const primaryFormat = perCollectionConfig.formats[0];
|
|
56
|
+
const converted = await convertFormat(processed.buffer, primaryFormat.format, primaryFormat.quality);
|
|
57
|
+
mainBuffer = converted.buffer;
|
|
58
|
+
mainSize = converted.size;
|
|
59
|
+
newFilename = `${path.parse(safeFilename).name}.${primaryFormat.format}`;
|
|
60
|
+
newMimeType = converted.mimeType;
|
|
61
|
+
}
|
|
62
|
+
// Write optimized file to disk
|
|
63
|
+
const newFilePath = path.join(staticDir, newFilename);
|
|
64
|
+
await fs.writeFile(newFilePath, mainBuffer);
|
|
65
|
+
// Clean up old file if filename changed
|
|
66
|
+
if (newFilename !== safeFilename) {
|
|
67
|
+
await fs.unlink(filePath).catch(()=>{});
|
|
68
|
+
}
|
|
51
69
|
// Step 2: Generate ThumbHash
|
|
52
70
|
let thumbHash;
|
|
53
71
|
if (resolvedConfig.generateThumbHash) {
|
|
54
|
-
thumbHash = await generateThumbHash(
|
|
72
|
+
thumbHash = await generateThumbHash(mainBuffer);
|
|
55
73
|
}
|
|
56
|
-
// Step 3: Convert to
|
|
74
|
+
// Step 3: Convert to configured formats (skip primary when replaceOriginal)
|
|
75
|
+
const formatsToGenerate = perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0 ? perCollectionConfig.formats.slice(1) : perCollectionConfig.formats;
|
|
57
76
|
const variants = [];
|
|
58
|
-
for (const format of
|
|
59
|
-
const result = await convertFormat(
|
|
60
|
-
const variantFilename = `${path.parse(
|
|
77
|
+
for (const format of formatsToGenerate){
|
|
78
|
+
const result = await convertFormat(mainBuffer, format.format, format.quality);
|
|
79
|
+
const variantFilename = `${path.parse(newFilename).name}-optimized.${format.format}`;
|
|
61
80
|
await fs.writeFile(path.join(staticDir, variantFilename), result.buffer);
|
|
62
81
|
variants.push({
|
|
63
82
|
format: format.format,
|
|
@@ -70,19 +89,26 @@ export const createRegenerateDocumentHandler = (resolvedConfig)=>{
|
|
|
70
89
|
});
|
|
71
90
|
}
|
|
72
91
|
// Step 4: Update the document with all optimization data
|
|
92
|
+
const updateData = {
|
|
93
|
+
imageOptimizer: {
|
|
94
|
+
originalSize,
|
|
95
|
+
optimizedSize: mainSize,
|
|
96
|
+
status: 'complete',
|
|
97
|
+
thumbHash,
|
|
98
|
+
variants,
|
|
99
|
+
error: null
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
// Update filename, mimeType, and filesize when replaceOriginal changed them
|
|
103
|
+
if (newFilename !== safeFilename) {
|
|
104
|
+
updateData.filename = newFilename;
|
|
105
|
+
updateData.filesize = mainSize;
|
|
106
|
+
updateData.mimeType = newMimeType;
|
|
107
|
+
}
|
|
73
108
|
await req.payload.update({
|
|
74
109
|
collection: input.collectionSlug,
|
|
75
110
|
id: input.docId,
|
|
76
|
-
data:
|
|
77
|
-
imageOptimizer: {
|
|
78
|
-
originalSize,
|
|
79
|
-
optimizedSize: processed.size,
|
|
80
|
-
status: 'complete',
|
|
81
|
-
thumbHash,
|
|
82
|
-
variants,
|
|
83
|
-
error: null
|
|
84
|
-
}
|
|
85
|
-
},
|
|
111
|
+
data: updateData,
|
|
86
112
|
context: {
|
|
87
113
|
imageOptimizer_skip: true
|
|
88
114
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tasks/regenerateDocument.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\n\nimport type { CollectionSlug } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { stripAndResize, generateThumbHash, convertFormat } from '../processing/index.js'\n\nexport const createRegenerateDocumentHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n return async ({ input, req }: { input: { collectionSlug: string; docId: string }; req: any }) => {\n try {\n const doc = await req.payload.findByID({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n })\n\n // Skip non-image documents\n if (!doc.mimeType || !doc.mimeType.startsWith('image/')) {\n return { output: { status: 'skipped', reason: 'not-image' } }\n }\n\n const collectionConfig = req.payload.collections[input.collectionSlug as keyof typeof req.payload.collections].config\n\n let staticDir: string =\n typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : ''\n if (!staticDir) {\n throw new Error(`No staticDir configured for collection \"${input.collectionSlug}\"`)\n }\n if (!path.isAbsolute(staticDir)) {\n staticDir = path.resolve(process.cwd(), staticDir)\n }\n\n // Sanitize filename to prevent path traversal\n const safeFilename = path.basename(doc.filename)\n const filePath = path.join(staticDir, safeFilename)\n\n let fileBuffer: Buffer\n try {\n fileBuffer = await fs.readFile(filePath)\n } catch {\n // If file not on disk, try fetching from URL\n if (doc.url) {\n const url = doc.url.startsWith('http')\n ? doc.url\n : `${process.env.NEXT_PUBLIC_SERVER_URL || ''}${doc.url}`\n const response = await fetch(url)\n fileBuffer = Buffer.from(await response.arrayBuffer())\n } else {\n throw new Error(`File not found: ${filePath}`)\n }\n }\n\n const originalSize = fileBuffer.length\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug)\n\n // Step 1: Strip metadata + resize\n const processed = await stripAndResize(\n fileBuffer,\n perCollectionConfig.maxDimensions,\n resolvedConfig.stripMetadata,\n )\n\n // Write optimized file back to disk\n await fs.writeFile(filePath, processed.buffer)\n\n // Step 2: Generate ThumbHash\n let thumbHash: string | undefined\n if (resolvedConfig.generateThumbHash) {\n thumbHash = await generateThumbHash(processed.buffer)\n }\n\n // Step 3: Convert to all configured formats\n const variants: Array<{\n filename: string\n filesize: number\n format: string\n height: number\n mimeType: string\n url: string\n width: number\n }> = []\n\n for (const format of perCollectionConfig.formats) {\n const result = await convertFormat(processed.buffer, format.format, format.quality)\n const variantFilename = `${path.parse(safeFilename).name}-optimized.${format.format}`\n await fs.writeFile(path.join(staticDir, variantFilename), result.buffer)\n\n variants.push({\n format: format.format,\n filename: variantFilename,\n filesize: result.size,\n width: result.width,\n height: result.height,\n mimeType: result.mimeType,\n url: `/api/${input.collectionSlug}/file/${variantFilename}`,\n })\n }\n\n // Step 4: Update the document with all optimization data\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n originalSize,\n optimizedSize: processed.size,\n status: 'complete',\n thumbHash,\n variants,\n error: null,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n\n return { output: { status: 'complete' } }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err)\n\n try {\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n status: 'error',\n error: errorMessage,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n } catch (updateErr) {\n req.payload.logger.error(\n { err: updateErr },\n 'Failed to persist error status for image optimizer regeneration',\n )\n }\n\n throw err\n }\n }\n}\n"],"names":["fs","path","resolveCollectionConfig","stripAndResize","generateThumbHash","convertFormat","createRegenerateDocumentHandler","resolvedConfig","input","req","doc","payload","findByID","collection","collectionSlug","id","docId","mimeType","startsWith","output","status","reason","collectionConfig","collections","config","staticDir","upload","Error","isAbsolute","resolve","process","cwd","safeFilename","basename","filename","filePath","join","fileBuffer","readFile","url","env","NEXT_PUBLIC_SERVER_URL","response","fetch","Buffer","from","arrayBuffer","originalSize","length","perCollectionConfig","processed","maxDimensions","stripMetadata","writeFile","buffer","thumbHash","variants","format","formats","result","quality","variantFilename","parse","name","push","filesize","size","width","height","update","data","imageOptimizer","optimizedSize","error","context","imageOptimizer_skip","err","errorMessage","message","String","updateErr","logger"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/tasks/regenerateDocument.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\n\nimport type { CollectionSlug } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { stripAndResize, generateThumbHash, convertFormat } from '../processing/index.js'\n\nexport const createRegenerateDocumentHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n return async ({ input, req }: { input: { collectionSlug: string; docId: string }; req: any }) => {\n try {\n const doc = await req.payload.findByID({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n })\n\n // Skip non-image documents\n if (!doc.mimeType || !doc.mimeType.startsWith('image/')) {\n return { output: { status: 'skipped', reason: 'not-image' } }\n }\n\n const collectionConfig = req.payload.collections[input.collectionSlug as keyof typeof req.payload.collections].config\n\n let staticDir: string =\n typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : ''\n if (!staticDir) {\n throw new Error(`No staticDir configured for collection \"${input.collectionSlug}\"`)\n }\n if (!path.isAbsolute(staticDir)) {\n staticDir = path.resolve(process.cwd(), staticDir)\n }\n\n // Sanitize filename to prevent path traversal\n const safeFilename = path.basename(doc.filename)\n const filePath = path.join(staticDir, safeFilename)\n\n let fileBuffer: Buffer\n try {\n fileBuffer = await fs.readFile(filePath)\n } catch {\n // If file not on disk, try fetching from URL\n if (doc.url) {\n const url = doc.url.startsWith('http')\n ? doc.url\n : `${process.env.NEXT_PUBLIC_SERVER_URL || ''}${doc.url}`\n const response = await fetch(url)\n fileBuffer = Buffer.from(await response.arrayBuffer())\n } else {\n throw new Error(`File not found: ${filePath}`)\n }\n }\n\n const originalSize = fileBuffer.length\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug)\n\n // Step 1: Strip metadata + resize\n const processed = await stripAndResize(\n fileBuffer,\n perCollectionConfig.maxDimensions,\n resolvedConfig.stripMetadata,\n )\n\n let mainBuffer = processed.buffer\n let mainSize = processed.size\n let newFilename = safeFilename\n let newMimeType: string | undefined\n\n // Step 1b: If replaceOriginal, convert main file to primary format\n if (perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0) {\n const primaryFormat = perCollectionConfig.formats[0]\n const converted = await convertFormat(processed.buffer, primaryFormat.format, primaryFormat.quality)\n mainBuffer = converted.buffer\n mainSize = converted.size\n newFilename = `${path.parse(safeFilename).name}.${primaryFormat.format}`\n newMimeType = converted.mimeType\n }\n\n // Write optimized file to disk\n const newFilePath = path.join(staticDir, newFilename)\n await fs.writeFile(newFilePath, mainBuffer)\n\n // Clean up old file if filename changed\n if (newFilename !== safeFilename) {\n await fs.unlink(filePath).catch(() => {})\n }\n\n // Step 2: Generate ThumbHash\n let thumbHash: string | undefined\n if (resolvedConfig.generateThumbHash) {\n thumbHash = await generateThumbHash(mainBuffer)\n }\n\n // Step 3: Convert to configured formats (skip primary when replaceOriginal)\n const formatsToGenerate = perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0\n ? perCollectionConfig.formats.slice(1)\n : perCollectionConfig.formats\n\n const variants: Array<{\n filename: string\n filesize: number\n format: string\n height: number\n mimeType: string\n url: string\n width: number\n }> = []\n\n for (const format of formatsToGenerate) {\n const result = await convertFormat(mainBuffer, format.format, format.quality)\n const variantFilename = `${path.parse(newFilename).name}-optimized.${format.format}`\n await fs.writeFile(path.join(staticDir, variantFilename), result.buffer)\n\n variants.push({\n format: format.format,\n filename: variantFilename,\n filesize: result.size,\n width: result.width,\n height: result.height,\n mimeType: result.mimeType,\n url: `/api/${input.collectionSlug}/file/${variantFilename}`,\n })\n }\n\n // Step 4: Update the document with all optimization data\n const updateData: Record<string, any> = {\n imageOptimizer: {\n originalSize,\n optimizedSize: mainSize,\n status: 'complete',\n thumbHash,\n variants,\n error: null,\n },\n }\n\n // Update filename, mimeType, and filesize when replaceOriginal changed them\n if (newFilename !== safeFilename) {\n updateData.filename = newFilename\n updateData.filesize = mainSize\n updateData.mimeType = newMimeType\n }\n\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: updateData,\n context: { imageOptimizer_skip: true },\n })\n\n return { output: { status: 'complete' } }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err)\n\n try {\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n status: 'error',\n error: errorMessage,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n } catch (updateErr) {\n req.payload.logger.error(\n { err: updateErr },\n 'Failed to persist error status for image optimizer regeneration',\n )\n }\n\n throw err\n }\n }\n}\n"],"names":["fs","path","resolveCollectionConfig","stripAndResize","generateThumbHash","convertFormat","createRegenerateDocumentHandler","resolvedConfig","input","req","doc","payload","findByID","collection","collectionSlug","id","docId","mimeType","startsWith","output","status","reason","collectionConfig","collections","config","staticDir","upload","Error","isAbsolute","resolve","process","cwd","safeFilename","basename","filename","filePath","join","fileBuffer","readFile","url","env","NEXT_PUBLIC_SERVER_URL","response","fetch","Buffer","from","arrayBuffer","originalSize","length","perCollectionConfig","processed","maxDimensions","stripMetadata","mainBuffer","buffer","mainSize","size","newFilename","newMimeType","replaceOriginal","formats","primaryFormat","converted","format","quality","parse","name","newFilePath","writeFile","unlink","catch","thumbHash","formatsToGenerate","slice","variants","result","variantFilename","push","filesize","width","height","updateData","imageOptimizer","optimizedSize","error","update","data","context","imageOptimizer_skip","err","errorMessage","message","String","updateErr","logger"],"mappings":"AAAA,OAAOA,QAAQ,cAAa;AAC5B,OAAOC,UAAU,OAAM;AAKvB,SAASC,uBAAuB,QAAQ,iBAAgB;AACxD,SAASC,cAAc,EAAEC,iBAAiB,EAAEC,aAAa,QAAQ,yBAAwB;AAEzF,OAAO,MAAMC,kCAAkC,CAACC;IAC9C,OAAO,OAAO,EAAEC,KAAK,EAAEC,GAAG,EAAkE;QAC1F,IAAI;YACF,MAAMC,MAAM,MAAMD,IAAIE,OAAO,CAACC,QAAQ,CAAC;gBACrCC,YAAYL,MAAMM,cAAc;gBAChCC,IAAIP,MAAMQ,KAAK;YACjB;YAEA,2BAA2B;YAC3B,IAAI,CAACN,IAAIO,QAAQ,IAAI,CAACP,IAAIO,QAAQ,CAACC,UAAU,CAAC,WAAW;gBACvD,OAAO;oBAAEC,QAAQ;wBAAEC,QAAQ;wBAAWC,QAAQ;oBAAY;gBAAE;YAC9D;YAEA,MAAMC,mBAAmBb,IAAIE,OAAO,CAACY,WAAW,CAACf,MAAMM,cAAc,CAAyC,CAACU,MAAM;YAErH,IAAIC,YACF,OAAOH,iBAAiBI,MAAM,KAAK,WAAWJ,iBAAiBI,MAAM,CAACD,SAAS,IAAI,KAAK;YAC1F,IAAI,CAACA,WAAW;gBACd,MAAM,IAAIE,MAAM,CAAC,wCAAwC,EAAEnB,MAAMM,cAAc,CAAC,CAAC,CAAC;YACpF;YACA,IAAI,CAACb,KAAK2B,UAAU,CAACH,YAAY;gBAC/BA,YAAYxB,KAAK4B,OAAO,CAACC,QAAQC,GAAG,IAAIN;YAC1C;YAEA,8CAA8C;YAC9C,MAAMO,eAAe/B,KAAKgC,QAAQ,CAACvB,IAAIwB,QAAQ;YAC/C,MAAMC,WAAWlC,KAAKmC,IAAI,CAACX,WAAWO;YAEtC,IAAIK;YACJ,IAAI;gBACFA,aAAa,MAAMrC,GAAGsC,QAAQ,CAACH;YACjC,EAAE,OAAM;gBACN,6CAA6C;gBAC7C,IAAIzB,IAAI6B,GAAG,EAAE;oBACX,MAAMA,MAAM7B,IAAI6B,GAAG,CAACrB,UAAU,CAAC,UAC3BR,IAAI6B,GAAG,GACP,GAAGT,QAAQU,GAAG,CAACC,sBAAsB,IAAI,KAAK/B,IAAI6B,GAAG,EAAE;oBAC3D,MAAMG,WAAW,MAAMC,MAAMJ;oBAC7BF,aAAaO,OAAOC,IAAI,CAAC,MAAMH,SAASI,WAAW;gBACrD,OAAO;oBACL,MAAM,IAAInB,MAAM,CAAC,gBAAgB,EAAEQ,UAAU;gBAC/C;YACF;YAEA,MAAMY,eAAeV,WAAWW,MAAM;YACtC,MAAMC,sBAAsB/C,wBAAwBK,gBAAgBC,MAAMM,cAAc;YAExF,kCAAkC;YAClC,MAAMoC,YAAY,MAAM/C,eACtBkC,YACAY,oBAAoBE,aAAa,EACjC5C,eAAe6C,aAAa;YAG9B,IAAIC,aAAaH,UAAUI,MAAM;YACjC,IAAIC,WAAWL,UAAUM,IAAI;YAC7B,IAAIC,cAAczB;YAClB,IAAI0B;YAEJ,mEAAmE;YACnE,IAAIT,oBAAoBU,eAAe,IAAIV,oBAAoBW,OAAO,CAACZ,MAAM,GAAG,GAAG;gBACjF,MAAMa,gBAAgBZ,oBAAoBW,OAAO,CAAC,EAAE;gBACpD,MAAME,YAAY,MAAMzD,cAAc6C,UAAUI,MAAM,EAAEO,cAAcE,MAAM,EAAEF,cAAcG,OAAO;gBACnGX,aAAaS,UAAUR,MAAM;gBAC7BC,WAAWO,UAAUN,IAAI;gBACzBC,cAAc,GAAGxD,KAAKgE,KAAK,CAACjC,cAAckC,IAAI,CAAC,CAAC,EAAEL,cAAcE,MAAM,EAAE;gBACxEL,cAAcI,UAAU7C,QAAQ;YAClC;YAEA,+BAA+B;YAC/B,MAAMkD,cAAclE,KAAKmC,IAAI,CAACX,WAAWgC;YACzC,MAAMzD,GAAGoE,SAAS,CAACD,aAAad;YAEhC,wCAAwC;YACxC,IAAII,gBAAgBzB,cAAc;gBAChC,MAAMhC,GAAGqE,MAAM,CAAClC,UAAUmC,KAAK,CAAC,KAAO;YACzC;YAEA,6BAA6B;YAC7B,IAAIC;YACJ,IAAIhE,eAAeH,iBAAiB,EAAE;gBACpCmE,YAAY,MAAMnE,kBAAkBiD;YACtC;YAEA,4EAA4E;YAC5E,MAAMmB,oBAAoBvB,oBAAoBU,eAAe,IAAIV,oBAAoBW,OAAO,CAACZ,MAAM,GAAG,IAClGC,oBAAoBW,OAAO,CAACa,KAAK,CAAC,KAClCxB,oBAAoBW,OAAO;YAE/B,MAAMc,WAQD,EAAE;YAEP,KAAK,MAAMX,UAAUS,kBAAmB;gBACtC,MAAMG,SAAS,MAAMtE,cAAcgD,YAAYU,OAAOA,MAAM,EAAEA,OAAOC,OAAO;gBAC5E,MAAMY,kBAAkB,GAAG3E,KAAKgE,KAAK,CAACR,aAAaS,IAAI,CAAC,WAAW,EAAEH,OAAOA,MAAM,EAAE;gBACpF,MAAM/D,GAAGoE,SAAS,CAACnE,KAAKmC,IAAI,CAACX,WAAWmD,kBAAkBD,OAAOrB,MAAM;gBAEvEoB,SAASG,IAAI,CAAC;oBACZd,QAAQA,OAAOA,MAAM;oBACrB7B,UAAU0C;oBACVE,UAAUH,OAAOnB,IAAI;oBACrBuB,OAAOJ,OAAOI,KAAK;oBACnBC,QAAQL,OAAOK,MAAM;oBACrB/D,UAAU0D,OAAO1D,QAAQ;oBACzBsB,KAAK,CAAC,KAAK,EAAE/B,MAAMM,cAAc,CAAC,MAAM,EAAE8D,iBAAiB;gBAC7D;YACF;YAEA,yDAAyD;YACzD,MAAMK,aAAkC;gBACtCC,gBAAgB;oBACdnC;oBACAoC,eAAe5B;oBACfnC,QAAQ;oBACRmD;oBACAG;oBACAU,OAAO;gBACT;YACF;YAEA,4EAA4E;YAC5E,IAAI3B,gBAAgBzB,cAAc;gBAChCiD,WAAW/C,QAAQ,GAAGuB;gBACtBwB,WAAWH,QAAQ,GAAGvB;gBACtB0B,WAAWhE,QAAQ,GAAGyC;YACxB;YAEA,MAAMjD,IAAIE,OAAO,CAAC0E,MAAM,CAAC;gBACvBxE,YAAYL,MAAMM,cAAc;gBAChCC,IAAIP,MAAMQ,KAAK;gBACfsE,MAAML;gBACNM,SAAS;oBAAEC,qBAAqB;gBAAK;YACvC;YAEA,OAAO;gBAAErE,QAAQ;oBAAEC,QAAQ;gBAAW;YAAE;QAC1C,EAAE,OAAOqE,KAAK;YACZ,MAAMC,eAAeD,eAAe9D,QAAQ8D,IAAIE,OAAO,GAAGC,OAAOH;YAEjE,IAAI;gBACF,MAAMhF,IAAIE,OAAO,CAAC0E,MAAM,CAAC;oBACvBxE,YAAYL,MAAMM,cAAc;oBAChCC,IAAIP,MAAMQ,KAAK;oBACfsE,MAAM;wBACJJ,gBAAgB;4BACd9D,QAAQ;4BACRgE,OAAOM;wBACT;oBACF;oBACAH,SAAS;wBAAEC,qBAAqB;oBAAK;gBACvC;YACF,EAAE,OAAOK,WAAW;gBAClBpF,IAAIE,OAAO,CAACmF,MAAM,CAACV,KAAK,CACtB;oBAAEK,KAAKI;gBAAU,GACjB;YAEJ;YAEA,MAAMJ;QACR;IACF;AACF,EAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export type CollectionOptimizerConfig = {
|
|
|
10
10
|
width: number;
|
|
11
11
|
height: number;
|
|
12
12
|
};
|
|
13
|
+
replaceOriginal?: boolean;
|
|
13
14
|
};
|
|
14
15
|
export type ImageOptimizerConfig = {
|
|
15
16
|
collections: Partial<Record<CollectionSlug, true | CollectionOptimizerConfig>>;
|
|
@@ -20,6 +21,7 @@ export type ImageOptimizerConfig = {
|
|
|
20
21
|
width: number;
|
|
21
22
|
height: number;
|
|
22
23
|
};
|
|
24
|
+
replaceOriginal?: boolean;
|
|
23
25
|
stripMetadata?: boolean;
|
|
24
26
|
};
|
|
25
27
|
export type ResolvedCollectionOptimizerConfig = {
|
|
@@ -28,8 +30,10 @@ export type ResolvedCollectionOptimizerConfig = {
|
|
|
28
30
|
width: number;
|
|
29
31
|
height: number;
|
|
30
32
|
};
|
|
33
|
+
replaceOriginal: boolean;
|
|
31
34
|
};
|
|
32
35
|
export type ResolvedImageOptimizerConfig = Required<Pick<ImageOptimizerConfig, 'formats' | 'generateThumbHash' | 'maxDimensions' | 'stripMetadata'>> & {
|
|
33
36
|
collections: ImageOptimizerConfig['collections'];
|
|
34
37
|
disabled: boolean;
|
|
38
|
+
replaceOriginal: boolean;
|
|
35
39
|
};
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionSlug } from 'payload'\n\nexport type ImageFormat = 'webp' | 'avif'\n\nexport type FormatQuality = {\n format: ImageFormat\n quality: number // 1-100\n}\n\nexport type CollectionOptimizerConfig = {\n formats?: FormatQuality[]\n maxDimensions?: { width: number; height: number }\n}\n\nexport type ImageOptimizerConfig = {\n collections: Partial<Record<CollectionSlug, true | CollectionOptimizerConfig>>\n disabled?: boolean\n formats?: FormatQuality[]\n generateThumbHash?: boolean\n maxDimensions?: { width: number; height: number }\n stripMetadata?: boolean\n}\n\nexport type ResolvedCollectionOptimizerConfig = {\n formats: FormatQuality[]\n maxDimensions: { width: number; height: number }\n}\n\nexport type ResolvedImageOptimizerConfig = Required<\n Pick<ImageOptimizerConfig, 'formats' | 'generateThumbHash' | 'maxDimensions' | 'stripMetadata'>\n> & {\n collections: ImageOptimizerConfig['collections']\n disabled: boolean\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionSlug } from 'payload'\n\nexport type ImageFormat = 'webp' | 'avif'\n\nexport type FormatQuality = {\n format: ImageFormat\n quality: number // 1-100\n}\n\nexport type CollectionOptimizerConfig = {\n formats?: FormatQuality[]\n maxDimensions?: { width: number; height: number }\n replaceOriginal?: boolean\n}\n\nexport type ImageOptimizerConfig = {\n collections: Partial<Record<CollectionSlug, true | CollectionOptimizerConfig>>\n disabled?: boolean\n formats?: FormatQuality[]\n generateThumbHash?: boolean\n maxDimensions?: { width: number; height: number }\n replaceOriginal?: boolean\n stripMetadata?: boolean\n}\n\nexport type ResolvedCollectionOptimizerConfig = {\n formats: FormatQuality[]\n maxDimensions: { width: number; height: number }\n replaceOriginal: boolean\n}\n\nexport type ResolvedImageOptimizerConfig = Required<\n Pick<ImageOptimizerConfig, 'formats' | 'generateThumbHash' | 'maxDimensions' | 'stripMetadata'>\n> & {\n collections: ImageOptimizerConfig['collections']\n disabled: boolean\n replaceOriginal: boolean\n}\n"],"names":[],"mappings":"AA+BA,WAMC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inoo-ch/payload-image-optimizer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Payload CMS plugin for automatic image optimization — WebP/AVIF conversion, resize, EXIF strip, ThumbHash placeholders, and bulk regeneration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -24,44 +24,28 @@
|
|
|
24
24
|
"type": "module",
|
|
25
25
|
"exports": {
|
|
26
26
|
".": {
|
|
27
|
-
"import": "./
|
|
28
|
-
"types": "./
|
|
29
|
-
"default": "./
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
30
|
},
|
|
31
31
|
"./client": {
|
|
32
|
-
"import": "./
|
|
33
|
-
"types": "./
|
|
34
|
-
"default": "./
|
|
32
|
+
"import": "./dist/exports/client.js",
|
|
33
|
+
"types": "./dist/exports/client.d.ts",
|
|
34
|
+
"default": "./dist/exports/client.js"
|
|
35
35
|
},
|
|
36
36
|
"./rsc": {
|
|
37
|
-
"import": "./
|
|
38
|
-
"types": "./
|
|
39
|
-
"default": "./
|
|
37
|
+
"import": "./dist/exports/rsc.js",
|
|
38
|
+
"types": "./dist/exports/rsc.d.ts",
|
|
39
|
+
"default": "./dist/exports/rsc.js"
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
|
-
"main": "./
|
|
43
|
-
"types": "./
|
|
42
|
+
"main": "./dist/index.js",
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
44
44
|
"files": [
|
|
45
|
-
"dist"
|
|
45
|
+
"dist",
|
|
46
|
+
"src",
|
|
47
|
+
"AGENT_DOCS.md"
|
|
46
48
|
],
|
|
47
|
-
"scripts": {
|
|
48
|
-
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
|
|
49
|
-
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
|
50
|
-
"build:types": "tsc --outDir dist --rootDir ./src",
|
|
51
|
-
"clean": "rimraf {dist,*.tsbuildinfo}",
|
|
52
|
-
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
53
|
-
"dev": "next dev dev --turbo",
|
|
54
|
-
"dev:generate-importmap": "pnpm dev:payload generate:importmap",
|
|
55
|
-
"dev:generate-types": "pnpm dev:payload generate:types",
|
|
56
|
-
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
|
57
|
-
"generate:importmap": "pnpm dev:generate-importmap",
|
|
58
|
-
"generate:types": "pnpm dev:generate-types",
|
|
59
|
-
"lint": "eslint",
|
|
60
|
-
"lint:fix": "eslint ./src --fix",
|
|
61
|
-
"test": "pnpm test:int && pnpm test:e2e",
|
|
62
|
-
"test:e2e": "playwright test",
|
|
63
|
-
"test:int": "vitest"
|
|
64
|
-
},
|
|
65
49
|
"devDependencies": {
|
|
66
50
|
"@eslint/eslintrc": "^3.2.0",
|
|
67
51
|
"@payloadcms/db-mongodb": "3.79.0",
|
|
@@ -106,36 +90,26 @@
|
|
|
106
90
|
"node": "^18.20.2 || >=20.9.0",
|
|
107
91
|
"pnpm": "^9 || ^10"
|
|
108
92
|
},
|
|
109
|
-
"publishConfig": {
|
|
110
|
-
"exports": {
|
|
111
|
-
".": {
|
|
112
|
-
"import": "./dist/index.js",
|
|
113
|
-
"types": "./dist/index.d.ts",
|
|
114
|
-
"default": "./dist/index.js"
|
|
115
|
-
},
|
|
116
|
-
"./client": {
|
|
117
|
-
"import": "./dist/exports/client.js",
|
|
118
|
-
"types": "./dist/exports/client.d.ts",
|
|
119
|
-
"default": "./dist/exports/client.js"
|
|
120
|
-
},
|
|
121
|
-
"./rsc": {
|
|
122
|
-
"import": "./dist/exports/rsc.js",
|
|
123
|
-
"types": "./dist/exports/rsc.d.ts",
|
|
124
|
-
"default": "./dist/exports/rsc.js"
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
"main": "./dist/index.js",
|
|
128
|
-
"types": "./dist/index.d.ts"
|
|
129
|
-
},
|
|
130
|
-
"pnpm": {
|
|
131
|
-
"onlyBuiltDependencies": [
|
|
132
|
-
"sharp",
|
|
133
|
-
"esbuild",
|
|
134
|
-
"unrs-resolver"
|
|
135
|
-
]
|
|
136
|
-
},
|
|
137
93
|
"registry": "https://registry.npmjs.org/",
|
|
138
94
|
"dependencies": {
|
|
139
95
|
"thumbhash": "^0.1.1"
|
|
96
|
+
},
|
|
97
|
+
"scripts": {
|
|
98
|
+
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
|
|
99
|
+
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
|
100
|
+
"build:types": "tsc --outDir dist --rootDir ./src",
|
|
101
|
+
"clean": "rimraf {dist,*.tsbuildinfo}",
|
|
102
|
+
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
103
|
+
"dev": "next dev dev --turbo",
|
|
104
|
+
"dev:generate-importmap": "pnpm dev:payload generate:importmap",
|
|
105
|
+
"dev:generate-types": "pnpm dev:payload generate:types",
|
|
106
|
+
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
|
107
|
+
"generate:importmap": "pnpm dev:generate-importmap",
|
|
108
|
+
"generate:types": "pnpm dev:generate-types",
|
|
109
|
+
"lint": "eslint",
|
|
110
|
+
"lint:fix": "eslint ./src --fix",
|
|
111
|
+
"test": "pnpm test:int && pnpm test:e2e",
|
|
112
|
+
"test:e2e": "playwright test",
|
|
113
|
+
"test:int": "vitest"
|
|
140
114
|
}
|
|
141
|
-
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import NextImage, { type ImageProps } from 'next/image'
|
|
5
|
+
import { getImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'
|
|
6
|
+
|
|
7
|
+
type ImageOptimizerData = {
|
|
8
|
+
thumbHash?: string | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type MediaResource = {
|
|
12
|
+
url?: string | null
|
|
13
|
+
alt?: string | null
|
|
14
|
+
width?: number | null
|
|
15
|
+
height?: number | null
|
|
16
|
+
filename?: string | null
|
|
17
|
+
focalX?: number | null
|
|
18
|
+
focalY?: number | null
|
|
19
|
+
imageOptimizer?: ImageOptimizerData | null
|
|
20
|
+
updatedAt?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ImageBoxProps extends Omit<ImageProps, 'src' | 'alt'> {
|
|
24
|
+
media: MediaResource | string
|
|
25
|
+
alt?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const ImageBox: React.FC<ImageBoxProps> = ({
|
|
29
|
+
media,
|
|
30
|
+
alt: altFromProps,
|
|
31
|
+
fill,
|
|
32
|
+
sizes,
|
|
33
|
+
priority,
|
|
34
|
+
loading: loadingFromProps,
|
|
35
|
+
style: styleFromProps,
|
|
36
|
+
...props
|
|
37
|
+
}) => {
|
|
38
|
+
const loading = priority ? undefined : (loadingFromProps ?? 'lazy')
|
|
39
|
+
|
|
40
|
+
if (typeof media === 'string') {
|
|
41
|
+
return (
|
|
42
|
+
<NextImage
|
|
43
|
+
{...props}
|
|
44
|
+
src={media}
|
|
45
|
+
alt={altFromProps || ''}
|
|
46
|
+
quality={80}
|
|
47
|
+
fill={fill}
|
|
48
|
+
sizes={sizes}
|
|
49
|
+
style={{ objectFit: 'cover', objectPosition: 'center', ...styleFromProps }}
|
|
50
|
+
priority={priority}
|
|
51
|
+
loading={loading}
|
|
52
|
+
/>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const width = media.width ?? undefined
|
|
57
|
+
const height = media.height ?? undefined
|
|
58
|
+
const alt = altFromProps || (media as any).alt || media.filename || ''
|
|
59
|
+
const src = media.url ? `${media.url}${media.updatedAt ? `?${media.updatedAt}` : ''}` : ''
|
|
60
|
+
|
|
61
|
+
const optimizerProps = getImageOptimizerProps(media)
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<NextImage
|
|
65
|
+
{...props}
|
|
66
|
+
src={src}
|
|
67
|
+
alt={alt}
|
|
68
|
+
quality={80}
|
|
69
|
+
fill={fill}
|
|
70
|
+
width={!fill ? width : undefined}
|
|
71
|
+
height={!fill ? height : undefined}
|
|
72
|
+
sizes={sizes}
|
|
73
|
+
style={{ objectFit: 'cover', ...optimizerProps.style, ...styleFromProps }}
|
|
74
|
+
placeholder={optimizerProps.placeholder}
|
|
75
|
+
blurDataURL={optimizerProps.blurDataURL}
|
|
76
|
+
priority={priority}
|
|
77
|
+
loading={loading}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { thumbHashToDataURL } from 'thumbhash'
|
|
5
|
+
import { useAllFormFields } from '@payloadcms/ui'
|
|
6
|
+
|
|
7
|
+
const formatBytes = (bytes: number): string => {
|
|
8
|
+
if (bytes === 0) return '0 B'
|
|
9
|
+
const k = 1024
|
|
10
|
+
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
11
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
12
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const statusColors: Record<string, string> = {
|
|
16
|
+
pending: '#f59e0b',
|
|
17
|
+
processing: '#3b82f6',
|
|
18
|
+
complete: '#10b981',
|
|
19
|
+
error: '#ef4444',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const OptimizationStatus: React.FC<{ path?: string }> = (props) => {
|
|
23
|
+
const [formState] = useAllFormFields()
|
|
24
|
+
const basePath = props.path ?? 'imageOptimizer'
|
|
25
|
+
|
|
26
|
+
const status = formState[`${basePath}.status`]?.value as string | undefined
|
|
27
|
+
const originalSize = formState[`${basePath}.originalSize`]?.value as number | undefined
|
|
28
|
+
const optimizedSize = formState[`${basePath}.optimizedSize`]?.value as number | undefined
|
|
29
|
+
const thumbHash = formState[`${basePath}.thumbHash`]?.value as string | undefined
|
|
30
|
+
const error = formState[`${basePath}.error`]?.value as string | undefined
|
|
31
|
+
|
|
32
|
+
const thumbHashUrl = React.useMemo(() => {
|
|
33
|
+
if (!thumbHash) return null
|
|
34
|
+
try {
|
|
35
|
+
const bytes = Uint8Array.from(atob(thumbHash), c => c.charCodeAt(0))
|
|
36
|
+
return thumbHashToDataURL(bytes)
|
|
37
|
+
} catch {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
}, [thumbHash])
|
|
41
|
+
|
|
42
|
+
// Read variants array from form state
|
|
43
|
+
const variantsField = formState[`${basePath}.variants`]
|
|
44
|
+
const rowCount = (variantsField as any)?.rows?.length ?? 0
|
|
45
|
+
const variants: Array<{
|
|
46
|
+
format?: string
|
|
47
|
+
filename?: string
|
|
48
|
+
filesize?: number
|
|
49
|
+
width?: number
|
|
50
|
+
height?: number
|
|
51
|
+
}> = []
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < rowCount; i++) {
|
|
54
|
+
variants.push({
|
|
55
|
+
format: formState[`${basePath}.variants.${i}.format`]?.value as string | undefined,
|
|
56
|
+
filename: formState[`${basePath}.variants.${i}.filename`]?.value as string | undefined,
|
|
57
|
+
filesize: formState[`${basePath}.variants.${i}.filesize`]?.value as number | undefined,
|
|
58
|
+
width: formState[`${basePath}.variants.${i}.width`]?.value as number | undefined,
|
|
59
|
+
height: formState[`${basePath}.variants.${i}.height`]?.value as number | undefined,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!status) {
|
|
64
|
+
return (
|
|
65
|
+
<div style={{ padding: '12px 0' }}>
|
|
66
|
+
<div style={{ color: '#6b7280', fontSize: '13px' }}>
|
|
67
|
+
No optimization data yet. Upload an image to optimize.
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const savings =
|
|
74
|
+
originalSize && optimizedSize
|
|
75
|
+
? Math.round((1 - optimizedSize / originalSize) * 100)
|
|
76
|
+
: null
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div style={{ padding: '12px 0' }}>
|
|
80
|
+
<div style={{ marginBottom: '8px' }}>
|
|
81
|
+
<span
|
|
82
|
+
style={{
|
|
83
|
+
backgroundColor: statusColors[status] || '#6b7280',
|
|
84
|
+
borderRadius: '4px',
|
|
85
|
+
color: '#fff',
|
|
86
|
+
display: 'inline-block',
|
|
87
|
+
fontSize: '12px',
|
|
88
|
+
fontWeight: 600,
|
|
89
|
+
padding: '2px 8px',
|
|
90
|
+
textTransform: 'uppercase',
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
{status}
|
|
94
|
+
</span>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{error && (
|
|
98
|
+
<div style={{ color: '#ef4444', fontSize: '13px', marginBottom: '8px' }}>{error}</div>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{originalSize != null && optimizedSize != null && (
|
|
102
|
+
<div style={{ fontSize: '13px', marginBottom: '8px' }}>
|
|
103
|
+
<div>Original: <strong>{formatBytes(originalSize)}</strong></div>
|
|
104
|
+
<div>
|
|
105
|
+
Optimized: <strong>{formatBytes(optimizedSize)}</strong>
|
|
106
|
+
{savings != null && savings > 0 && (
|
|
107
|
+
<span style={{ color: '#10b981', marginLeft: '4px' }}>(-{savings}%)</span>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{thumbHashUrl && (
|
|
114
|
+
<div style={{ marginBottom: '8px' }}>
|
|
115
|
+
<div style={{ fontSize: '12px', marginBottom: '4px', opacity: 0.7 }}>Blur Preview</div>
|
|
116
|
+
<img
|
|
117
|
+
alt="Blur placeholder"
|
|
118
|
+
src={thumbHashUrl}
|
|
119
|
+
style={{ borderRadius: '4px', height: '40px', width: 'auto' }}
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
{variants.length > 0 && (
|
|
125
|
+
<div>
|
|
126
|
+
<div style={{ fontSize: '12px', marginBottom: '4px', opacity: 0.7 }}>Variants</div>
|
|
127
|
+
{variants.map((v, i) => (
|
|
128
|
+
<div key={i} style={{ fontSize: '12px', marginBottom: '2px' }}>
|
|
129
|
+
<strong>{v.format?.toUpperCase()}</strong> — {v.filesize ? formatBytes(v.filesize) : '?'}{' '}
|
|
130
|
+
({v.width}x{v.height})
|
|
131
|
+
</div>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
)
|
|
137
|
+
}
|