@inoo-ch/payload-image-optimizer 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ImageBox.d.ts +1 -15
- package/dist/components/ImageBox.js.map +1 -1
- package/dist/components/OptimizationStatus.js +84 -19
- package/dist/components/OptimizationStatus.js.map +1 -1
- package/dist/fields/imageOptimizerField.d.ts +4 -2
- package/dist/fields/imageOptimizerField.js +57 -54
- package/dist/fields/imageOptimizerField.js.map +1 -1
- package/dist/hooks/afterChange.js +2 -4
- package/dist/hooks/afterChange.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +123 -105
- package/dist/index.js.map +1 -1
- package/dist/tasks/convertFormats.js +2 -4
- package/dist/tasks/convertFormats.js.map +1 -1
- package/dist/tasks/regenerateDocument.js +2 -4
- package/dist/tasks/regenerateDocument.js.map +1 -1
- package/dist/translations/index.d.ts +1 -0
- package/dist/translations/index.js +64 -0
- package/dist/translations/index.js.map +1 -0
- package/dist/types.d.ts +19 -1
- package/dist/types.js.map +1 -1
- package/dist/utilities/getImageOptimizerProps.d.ts +2 -10
- package/dist/utilities/getImageOptimizerProps.js.map +1 -1
- package/dist/utilities/resolveStaticDir.d.ts +3 -0
- package/dist/utilities/resolveStaticDir.js +10 -0
- package/dist/utilities/resolveStaticDir.js.map +1 -0
- package/package.json +3 -2
- package/src/components/ImageBox.tsx +1 -16
- package/src/fields/imageOptimizerField.ts +65 -59
- package/src/hooks/afterChange.ts +43 -23
- package/src/hooks/beforeChange.ts +11 -2
- package/src/index.ts +94 -95
- package/src/tasks/convertFormats.ts +24 -9
- package/src/tasks/regenerateDocument.ts +93 -79
- package/src/translations/index.ts +62 -0
- package/src/types.ts +20 -1
- package/src/utilities/getImageOptimizerProps.ts +2 -10
- package/src/utilities/resolveStaticDir.ts +12 -0
- package/src/utilities/storage.ts +50 -0
|
@@ -1,22 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { type ImageProps } from 'next/image';
|
|
3
|
-
type
|
|
4
|
-
thumbHash?: string | null;
|
|
5
|
-
};
|
|
6
|
-
type MediaResource = {
|
|
7
|
-
url?: string | null;
|
|
8
|
-
alt?: string | null;
|
|
9
|
-
width?: number | null;
|
|
10
|
-
height?: number | null;
|
|
11
|
-
filename?: string | null;
|
|
12
|
-
focalX?: number | null;
|
|
13
|
-
focalY?: number | null;
|
|
14
|
-
imageOptimizer?: ImageOptimizerData | null;
|
|
15
|
-
updatedAt?: string;
|
|
16
|
-
};
|
|
3
|
+
import type { MediaResource } from '../types.js';
|
|
17
4
|
export interface ImageBoxProps extends Omit<ImageProps, 'src' | 'alt'> {
|
|
18
5
|
media: MediaResource | string;
|
|
19
6
|
alt?: string;
|
|
20
7
|
}
|
|
21
8
|
export declare const ImageBox: React.FC<ImageBoxProps>;
|
|
22
|
-
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/ImageBox.tsx"],"sourcesContent":["'use client'\n\nimport React from 'react'\nimport NextImage, { type ImageProps } from 'next/image'\nimport {
|
|
1
|
+
{"version":3,"sources":["../../src/components/ImageBox.tsx"],"sourcesContent":["'use client'\n\nimport React from 'react'\nimport NextImage, { type ImageProps } from 'next/image'\nimport type { MediaResource } from '../types.js'\nimport { getImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'\n\nexport interface ImageBoxProps extends Omit<ImageProps, 'src' | 'alt'> {\n media: MediaResource | string\n alt?: string\n}\n\nexport const ImageBox: React.FC<ImageBoxProps> = ({\n media,\n alt: altFromProps,\n fill,\n sizes,\n priority,\n loading: loadingFromProps,\n style: styleFromProps,\n ...props\n}) => {\n const loading = priority ? undefined : (loadingFromProps ?? 'lazy')\n\n if (typeof media === 'string') {\n return (\n <NextImage\n {...props}\n src={media}\n alt={altFromProps || ''}\n quality={80}\n fill={fill}\n sizes={sizes}\n style={{ objectFit: 'cover', objectPosition: 'center', ...styleFromProps }}\n priority={priority}\n loading={loading}\n />\n )\n }\n\n const width = media.width ?? undefined\n const height = media.height ?? undefined\n const alt = altFromProps || (media as any).alt || media.filename || ''\n const src = media.url ? `${media.url}${media.updatedAt ? `?${media.updatedAt}` : ''}` : ''\n\n const optimizerProps = getImageOptimizerProps(media)\n\n return (\n <NextImage\n {...props}\n src={src}\n alt={alt}\n quality={80}\n fill={fill}\n width={!fill ? width : undefined}\n height={!fill ? height : undefined}\n sizes={sizes}\n style={{ objectFit: 'cover', ...optimizerProps.style, ...styleFromProps }}\n placeholder={optimizerProps.placeholder}\n blurDataURL={optimizerProps.blurDataURL}\n priority={priority}\n loading={loading}\n />\n )\n}\n"],"names":["React","NextImage","getImageOptimizerProps","ImageBox","media","alt","altFromProps","fill","sizes","priority","loading","loadingFromProps","style","styleFromProps","props","undefined","src","quality","objectFit","objectPosition","width","height","filename","url","updatedAt","optimizerProps","placeholder","blurDataURL"],"mappings":"AAAA;;AAEA,OAAOA,WAAW,QAAO;AACzB,OAAOC,eAAoC,aAAY;AAEvD,SAASC,sBAAsB,QAAQ,yCAAwC;AAO/E,OAAO,MAAMC,WAAoC,CAAC,EAChDC,KAAK,EACLC,KAAKC,YAAY,EACjBC,IAAI,EACJC,KAAK,EACLC,QAAQ,EACRC,SAASC,gBAAgB,EACzBC,OAAOC,cAAc,EACrB,GAAGC,OACJ;IACC,MAAMJ,UAAUD,WAAWM,YAAaJ,oBAAoB;IAE5D,IAAI,OAAOP,UAAU,UAAU;QAC7B,qBACE,KAACH;YACE,GAAGa,KAAK;YACTE,KAAKZ;YACLC,KAAKC,gBAAgB;YACrBW,SAAS;YACTV,MAAMA;YACNC,OAAOA;YACPI,OAAO;gBAAEM,WAAW;gBAASC,gBAAgB;gBAAU,GAAGN,cAAc;YAAC;YACzEJ,UAAUA;YACVC,SAASA;;IAGf;IAEA,MAAMU,QAAQhB,MAAMgB,KAAK,IAAIL;IAC7B,MAAMM,SAASjB,MAAMiB,MAAM,IAAIN;IAC/B,MAAMV,MAAMC,gBAAgB,AAACF,MAAcC,GAAG,IAAID,MAAMkB,QAAQ,IAAI;IACpE,MAAMN,MAAMZ,MAAMmB,GAAG,GAAG,GAAGnB,MAAMmB,GAAG,GAAGnB,MAAMoB,SAAS,GAAG,CAAC,CAAC,EAAEpB,MAAMoB,SAAS,EAAE,GAAG,IAAI,GAAG;IAExF,MAAMC,iBAAiBvB,uBAAuBE;IAE9C,qBACE,KAACH;QACE,GAAGa,KAAK;QACTE,KAAKA;QACLX,KAAKA;QACLY,SAAS;QACTV,MAAMA;QACNa,OAAO,CAACb,OAAOa,QAAQL;QACvBM,QAAQ,CAACd,OAAOc,SAASN;QACzBP,OAAOA;QACPI,OAAO;YAAEM,WAAW;YAAS,GAAGO,eAAeb,KAAK;YAAE,GAAGC,cAAc;QAAC;QACxEa,aAAaD,eAAeC,WAAW;QACvCC,aAAaF,eAAeE,WAAW;QACvClB,UAAUA;QACVC,SAASA;;AAGf,EAAC"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { thumbHashToDataURL } from 'thumbhash';
|
|
5
|
-
import { useAllFormFields } from '@payloadcms/ui';
|
|
5
|
+
import { useAllFormFields, useDocumentInfo } from '@payloadcms/ui';
|
|
6
6
|
const formatBytes = (bytes)=>{
|
|
7
7
|
if (bytes === 0) return '0 B';
|
|
8
8
|
const k = 1024;
|
|
@@ -21,14 +21,71 @@ const statusColors = {
|
|
|
21
21
|
complete: '#10b981',
|
|
22
22
|
error: '#ef4444'
|
|
23
23
|
};
|
|
24
|
+
const POLL_INTERVAL_MS = 2000;
|
|
24
25
|
export const OptimizationStatus = (props)=>{
|
|
25
26
|
const [formState] = useAllFormFields();
|
|
27
|
+
const { collectionSlug, id } = useDocumentInfo();
|
|
26
28
|
const basePath = props.path ?? 'imageOptimizer';
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
29
|
+
const formStatus = formState[`${basePath}.status`]?.value;
|
|
30
|
+
const formOriginalSize = formState[`${basePath}.originalSize`]?.value;
|
|
31
|
+
const formOptimizedSize = formState[`${basePath}.optimizedSize`]?.value;
|
|
32
|
+
const formThumbHash = formState[`${basePath}.thumbHash`]?.value;
|
|
33
|
+
const formError = formState[`${basePath}.error`]?.value;
|
|
34
|
+
const [polledData, setPolledData] = React.useState(null);
|
|
35
|
+
// Reset polled data when a new upload changes the form status back to pending
|
|
36
|
+
React.useEffect(()=>{
|
|
37
|
+
if (formStatus === 'pending') {
|
|
38
|
+
setPolledData(null);
|
|
39
|
+
}
|
|
40
|
+
}, [
|
|
41
|
+
formStatus
|
|
42
|
+
]);
|
|
43
|
+
// Poll for status updates when status is non-terminal
|
|
44
|
+
React.useEffect(()=>{
|
|
45
|
+
const currentStatus = polledData?.status ?? formStatus;
|
|
46
|
+
if (!currentStatus || currentStatus === 'complete' || currentStatus === 'error') return;
|
|
47
|
+
if (!collectionSlug || !id) return;
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
const poll = async ()=>{
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch(`/api/${collectionSlug}/${id}?depth=0`, {
|
|
52
|
+
signal: controller.signal
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) return;
|
|
55
|
+
const doc = await res.json();
|
|
56
|
+
const optimizer = doc.imageOptimizer;
|
|
57
|
+
if (!optimizer) return;
|
|
58
|
+
setPolledData({
|
|
59
|
+
status: optimizer.status,
|
|
60
|
+
originalSize: optimizer.originalSize,
|
|
61
|
+
optimizedSize: optimizer.optimizedSize,
|
|
62
|
+
thumbHash: optimizer.thumbHash,
|
|
63
|
+
error: optimizer.error,
|
|
64
|
+
variants: optimizer.variants
|
|
65
|
+
});
|
|
66
|
+
} catch {
|
|
67
|
+
// Silently ignore fetch errors (abort, network issues)
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const intervalId = setInterval(poll, POLL_INTERVAL_MS);
|
|
71
|
+
// Run immediately on mount
|
|
72
|
+
poll();
|
|
73
|
+
return ()=>{
|
|
74
|
+
controller.abort();
|
|
75
|
+
clearInterval(intervalId);
|
|
76
|
+
};
|
|
77
|
+
}, [
|
|
78
|
+
polledData?.status,
|
|
79
|
+
formStatus,
|
|
80
|
+
collectionSlug,
|
|
81
|
+
id
|
|
82
|
+
]);
|
|
83
|
+
// Use polled data when available, otherwise fall back to form state
|
|
84
|
+
const status = polledData?.status ?? formStatus;
|
|
85
|
+
const originalSize = polledData?.originalSize ?? formOriginalSize;
|
|
86
|
+
const optimizedSize = polledData?.optimizedSize ?? formOptimizedSize;
|
|
87
|
+
const thumbHash = polledData?.thumbHash ?? formThumbHash;
|
|
88
|
+
const error = polledData?.error ?? formError;
|
|
32
89
|
const thumbHashUrl = React.useMemo(()=>{
|
|
33
90
|
if (!thumbHash) return null;
|
|
34
91
|
try {
|
|
@@ -40,19 +97,27 @@ export const OptimizationStatus = (props)=>{
|
|
|
40
97
|
}, [
|
|
41
98
|
thumbHash
|
|
42
99
|
]);
|
|
43
|
-
// Read variants
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
100
|
+
// Read variants from polled data or form state
|
|
101
|
+
const variants = React.useMemo(()=>{
|
|
102
|
+
if (polledData?.variants) return polledData.variants;
|
|
103
|
+
const variantsField = formState[`${basePath}.variants`];
|
|
104
|
+
const rowCount = variantsField?.rows?.length ?? 0;
|
|
105
|
+
const formVariants = [];
|
|
106
|
+
for(let i = 0; i < rowCount; i++){
|
|
107
|
+
formVariants.push({
|
|
108
|
+
format: formState[`${basePath}.variants.${i}.format`]?.value,
|
|
109
|
+
filename: formState[`${basePath}.variants.${i}.filename`]?.value,
|
|
110
|
+
filesize: formState[`${basePath}.variants.${i}.filesize`]?.value,
|
|
111
|
+
width: formState[`${basePath}.variants.${i}.width`]?.value,
|
|
112
|
+
height: formState[`${basePath}.variants.${i}.height`]?.value
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return formVariants;
|
|
116
|
+
}, [
|
|
117
|
+
polledData?.variants,
|
|
118
|
+
formState,
|
|
119
|
+
basePath
|
|
120
|
+
]);
|
|
56
121
|
if (!status) {
|
|
57
122
|
return /*#__PURE__*/ _jsx("div", {
|
|
58
123
|
style: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/OptimizationStatus.tsx"],"sourcesContent":["'use client'\n\nimport React from 'react'\nimport { thumbHashToDataURL } from 'thumbhash'\nimport { useAllFormFields } from '@payloadcms/ui'\n\nconst formatBytes = (bytes: number): string => {\n if (bytes === 0) return '0 B'\n const k = 1024\n const sizes = ['B', 'KB', 'MB', 'GB']\n const i = Math.floor(Math.log(bytes) / Math.log(k))\n return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`\n}\n\nconst statusColors: Record<string, string> = {\n pending: '#f59e0b',\n processing: '#3b82f6',\n complete: '#10b981',\n error: '#ef4444',\n}\n\nexport const OptimizationStatus: React.FC<{ path?: string }> = (props) => {\n const [formState] = useAllFormFields()\n const basePath = props.path ?? 'imageOptimizer'\n\n const status = formState[`${basePath}.status`]?.value as string | undefined\n const originalSize = formState[`${basePath}.originalSize`]?.value as number | undefined\n const optimizedSize = formState[`${basePath}.optimizedSize`]?.value as number | undefined\n const thumbHash = formState[`${basePath}.thumbHash`]?.value as string | undefined\n const error = formState[`${basePath}.error`]?.value as string | undefined\n\n const thumbHashUrl = React.useMemo(() => {\n if (!thumbHash) return null\n try {\n const bytes = Uint8Array.from(atob(thumbHash), c => c.charCodeAt(0))\n return thumbHashToDataURL(bytes)\n } catch {\n return null\n }\n }, [thumbHash])\n\n // Read variants array from form state\n const variantsField = formState[`${basePath}.variants`]\n const rowCount = (variantsField as any)?.rows?.length ?? 0\n const variants: Array<{\n format?: string\n filename?: string\n filesize?: number\n width?: number\n height?: number\n }> = []\n\n for (let i = 0; i < rowCount; i++) {\n variants.push({\n format: formState[`${basePath}.variants.${i}.format`]?.value as string | undefined,\n filename: formState[`${basePath}.variants.${i}.filename`]?.value as string | undefined,\n filesize: formState[`${basePath}.variants.${i}.filesize`]?.value as number | undefined,\n width: formState[`${basePath}.variants.${i}.width`]?.value as number | undefined,\n height: formState[`${basePath}.variants.${i}.height`]?.value as number | undefined,\n })\n }\n\n if (!status) {\n return (\n <div style={{ padding: '12px 0' }}>\n <div style={{ color: '#6b7280', fontSize: '13px' }}>\n No optimization data yet. Upload an image to optimize.\n </div>\n </div>\n )\n }\n\n const savings =\n originalSize && optimizedSize\n ? Math.round((1 - optimizedSize / originalSize) * 100)\n : null\n\n return (\n <div style={{ padding: '12px 0' }}>\n <div style={{ marginBottom: '8px' }}>\n <span\n style={{\n backgroundColor: statusColors[status] || '#6b7280',\n borderRadius: '4px',\n color: '#fff',\n display: 'inline-block',\n fontSize: '12px',\n fontWeight: 600,\n padding: '2px 8px',\n textTransform: 'uppercase',\n }}\n >\n {status}\n </span>\n </div>\n\n {error && (\n <div style={{ color: '#ef4444', fontSize: '13px', marginBottom: '8px' }}>{error}</div>\n )}\n\n {originalSize != null && optimizedSize != null && (\n <div style={{ fontSize: '13px', marginBottom: '8px' }}>\n <div>Original: <strong>{formatBytes(originalSize)}</strong></div>\n <div>\n Optimized: <strong>{formatBytes(optimizedSize)}</strong>\n {savings != null && savings > 0 && (\n <span style={{ color: '#10b981', marginLeft: '4px' }}>(-{savings}%)</span>\n )}\n </div>\n </div>\n )}\n\n {thumbHashUrl && (\n <div style={{ marginBottom: '8px' }}>\n <div style={{ fontSize: '12px', marginBottom: '4px', opacity: 0.7 }}>Blur Preview</div>\n <img\n alt=\"Blur placeholder\"\n src={thumbHashUrl}\n style={{ borderRadius: '4px', height: '40px', width: 'auto' }}\n />\n </div>\n )}\n\n {variants.length > 0 && (\n <div>\n <div style={{ fontSize: '12px', marginBottom: '4px', opacity: 0.7 }}>Variants</div>\n {variants.map((v, i) => (\n <div key={i} style={{ fontSize: '12px', marginBottom: '2px' }}>\n <strong>{v.format?.toUpperCase()}</strong> — {v.filesize ? formatBytes(v.filesize) : '?'}{' '}\n ({v.width}x{v.height})\n </div>\n ))}\n </div>\n )}\n </div>\n )\n}\n"],"names":["React","thumbHashToDataURL","useAllFormFields","formatBytes","bytes","k","sizes","i","Math","floor","log","parseFloat","pow","toFixed","statusColors","pending","processing","complete","error","OptimizationStatus","props","formState","basePath","path","status","value","originalSize","optimizedSize","thumbHash","thumbHashUrl","useMemo","Uint8Array","from","atob","c","charCodeAt","variantsField","rowCount","rows","length","variants","push","format","filename","filesize","width","height","div","style","padding","color","fontSize","savings","round","marginBottom","span","backgroundColor","borderRadius","display","fontWeight","textTransform","strong","marginLeft","opacity","img","alt","src","map","v","toUpperCase"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/components/OptimizationStatus.tsx"],"sourcesContent":["'use client'\n\nimport React from 'react'\nimport { thumbHashToDataURL } from 'thumbhash'\nimport { useAllFormFields, useDocumentInfo } from '@payloadcms/ui'\n\nconst formatBytes = (bytes: number): string => {\n if (bytes === 0) return '0 B'\n const k = 1024\n const sizes = ['B', 'KB', 'MB', 'GB']\n const i = Math.floor(Math.log(bytes) / Math.log(k))\n return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`\n}\n\nconst statusColors: Record<string, string> = {\n pending: '#f59e0b',\n processing: '#3b82f6',\n complete: '#10b981',\n error: '#ef4444',\n}\n\nconst POLL_INTERVAL_MS = 2000\n\ntype PolledData = {\n status?: string\n originalSize?: number\n optimizedSize?: number\n thumbHash?: string\n error?: string\n variants?: Array<{\n format?: string\n filename?: string\n filesize?: number\n width?: number\n height?: number\n }>\n}\n\nexport const OptimizationStatus: React.FC<{ path?: string }> = (props) => {\n const [formState] = useAllFormFields()\n const { collectionSlug, id } = useDocumentInfo()\n const basePath = props.path ?? 'imageOptimizer'\n\n const formStatus = formState[`${basePath}.status`]?.value as string | undefined\n const formOriginalSize = formState[`${basePath}.originalSize`]?.value as number | undefined\n const formOptimizedSize = formState[`${basePath}.optimizedSize`]?.value as number | undefined\n const formThumbHash = formState[`${basePath}.thumbHash`]?.value as string | undefined\n const formError = formState[`${basePath}.error`]?.value as string | undefined\n\n const [polledData, setPolledData] = React.useState<PolledData | null>(null)\n\n // Reset polled data when a new upload changes the form status back to pending\n React.useEffect(() => {\n if (formStatus === 'pending') {\n setPolledData(null)\n }\n }, [formStatus])\n\n // Poll for status updates when status is non-terminal\n React.useEffect(() => {\n const currentStatus = polledData?.status ?? formStatus\n if (!currentStatus || currentStatus === 'complete' || currentStatus === 'error') return\n if (!collectionSlug || !id) return\n\n const controller = new AbortController()\n\n const poll = async () => {\n try {\n const res = await fetch(`/api/${collectionSlug}/${id}?depth=0`, {\n signal: controller.signal,\n })\n if (!res.ok) return\n const doc = await res.json()\n const optimizer = doc.imageOptimizer\n if (!optimizer) return\n\n setPolledData({\n status: optimizer.status,\n originalSize: optimizer.originalSize,\n optimizedSize: optimizer.optimizedSize,\n thumbHash: optimizer.thumbHash,\n error: optimizer.error,\n variants: optimizer.variants,\n })\n } catch {\n // Silently ignore fetch errors (abort, network issues)\n }\n }\n\n const intervalId = setInterval(poll, POLL_INTERVAL_MS)\n // Run immediately on mount\n poll()\n\n return () => {\n controller.abort()\n clearInterval(intervalId)\n }\n }, [polledData?.status, formStatus, collectionSlug, id])\n\n // Use polled data when available, otherwise fall back to form state\n const status = polledData?.status ?? formStatus\n const originalSize = polledData?.originalSize ?? formOriginalSize\n const optimizedSize = polledData?.optimizedSize ?? formOptimizedSize\n const thumbHash = polledData?.thumbHash ?? formThumbHash\n const error = polledData?.error ?? formError\n\n const thumbHashUrl = React.useMemo(() => {\n if (!thumbHash) return null\n try {\n const bytes = Uint8Array.from(atob(thumbHash), c => c.charCodeAt(0))\n return thumbHashToDataURL(bytes)\n } catch {\n return null\n }\n }, [thumbHash])\n\n // Read variants from polled data or form state\n const variants: Array<{\n format?: string\n filename?: string\n filesize?: number\n width?: number\n height?: number\n }> = React.useMemo(() => {\n if (polledData?.variants) return polledData.variants\n\n const variantsField = formState[`${basePath}.variants`]\n const rowCount = (variantsField as any)?.rows?.length ?? 0\n const formVariants: typeof variants = []\n for (let i = 0; i < rowCount; i++) {\n formVariants.push({\n format: formState[`${basePath}.variants.${i}.format`]?.value as string | undefined,\n filename: formState[`${basePath}.variants.${i}.filename`]?.value as string | undefined,\n filesize: formState[`${basePath}.variants.${i}.filesize`]?.value as number | undefined,\n width: formState[`${basePath}.variants.${i}.width`]?.value as number | undefined,\n height: formState[`${basePath}.variants.${i}.height`]?.value as number | undefined,\n })\n }\n return formVariants\n }, [polledData?.variants, formState, basePath])\n\n if (!status) {\n return (\n <div style={{ padding: '12px 0' }}>\n <div style={{ color: '#6b7280', fontSize: '13px' }}>\n No optimization data yet. Upload an image to optimize.\n </div>\n </div>\n )\n }\n\n const savings =\n originalSize && optimizedSize\n ? Math.round((1 - optimizedSize / originalSize) * 100)\n : null\n\n return (\n <div style={{ padding: '12px 0' }}>\n <div style={{ marginBottom: '8px' }}>\n <span\n style={{\n backgroundColor: statusColors[status] || '#6b7280',\n borderRadius: '4px',\n color: '#fff',\n display: 'inline-block',\n fontSize: '12px',\n fontWeight: 600,\n padding: '2px 8px',\n textTransform: 'uppercase',\n }}\n >\n {status}\n </span>\n </div>\n\n {error && (\n <div style={{ color: '#ef4444', fontSize: '13px', marginBottom: '8px' }}>{error}</div>\n )}\n\n {originalSize != null && optimizedSize != null && (\n <div style={{ fontSize: '13px', marginBottom: '8px' }}>\n <div>Original: <strong>{formatBytes(originalSize)}</strong></div>\n <div>\n Optimized: <strong>{formatBytes(optimizedSize)}</strong>\n {savings != null && savings > 0 && (\n <span style={{ color: '#10b981', marginLeft: '4px' }}>(-{savings}%)</span>\n )}\n </div>\n </div>\n )}\n\n {thumbHashUrl && (\n <div style={{ marginBottom: '8px' }}>\n <div style={{ fontSize: '12px', marginBottom: '4px', opacity: 0.7 }}>Blur Preview</div>\n <img\n alt=\"Blur placeholder\"\n src={thumbHashUrl}\n style={{ borderRadius: '4px', height: '40px', width: 'auto' }}\n />\n </div>\n )}\n\n {variants.length > 0 && (\n <div>\n <div style={{ fontSize: '12px', marginBottom: '4px', opacity: 0.7 }}>Variants</div>\n {variants.map((v, i) => (\n <div key={i} style={{ fontSize: '12px', marginBottom: '2px' }}>\n <strong>{v.format?.toUpperCase()}</strong> — {v.filesize ? formatBytes(v.filesize) : '?'}{' '}\n ({v.width}x{v.height})\n </div>\n ))}\n </div>\n )}\n </div>\n )\n}\n"],"names":["React","thumbHashToDataURL","useAllFormFields","useDocumentInfo","formatBytes","bytes","k","sizes","i","Math","floor","log","parseFloat","pow","toFixed","statusColors","pending","processing","complete","error","POLL_INTERVAL_MS","OptimizationStatus","props","formState","collectionSlug","id","basePath","path","formStatus","value","formOriginalSize","formOptimizedSize","formThumbHash","formError","polledData","setPolledData","useState","useEffect","currentStatus","status","controller","AbortController","poll","res","fetch","signal","ok","doc","json","optimizer","imageOptimizer","originalSize","optimizedSize","thumbHash","variants","intervalId","setInterval","abort","clearInterval","thumbHashUrl","useMemo","Uint8Array","from","atob","c","charCodeAt","variantsField","rowCount","rows","length","formVariants","push","format","filename","filesize","width","height","div","style","padding","color","fontSize","savings","round","marginBottom","span","backgroundColor","borderRadius","display","fontWeight","textTransform","strong","marginLeft","opacity","img","alt","src","map","v","toUpperCase"],"mappings":"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"}
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
import type { GroupField } from 'payload';
|
|
2
|
-
|
|
1
|
+
import type { Field, GroupField } from 'payload';
|
|
2
|
+
import type { FieldsOverride } from '../types.js';
|
|
3
|
+
export declare const defaultImageOptimizerFields: Field[];
|
|
4
|
+
export declare const getImageOptimizerField: (fieldsOverride?: FieldsOverride) => GroupField;
|
|
@@ -1,75 +1,78 @@
|
|
|
1
|
-
export const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
export const defaultImageOptimizerFields = [
|
|
2
|
+
{
|
|
3
|
+
name: 'thumbHash',
|
|
4
|
+
type: 'text'
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
name: 'originalSize',
|
|
8
|
+
type: 'number'
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: 'optimizedSize',
|
|
12
|
+
type: 'number'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'status',
|
|
16
|
+
type: 'select',
|
|
17
|
+
options: [
|
|
18
|
+
'pending',
|
|
19
|
+
'processing',
|
|
20
|
+
'complete',
|
|
21
|
+
'error'
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'error',
|
|
26
|
+
type: 'text'
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'variants',
|
|
30
|
+
type: 'array',
|
|
11
31
|
fields: [
|
|
12
32
|
{
|
|
13
|
-
name: '
|
|
33
|
+
name: 'format',
|
|
34
|
+
type: 'text'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'filename',
|
|
14
38
|
type: 'text'
|
|
15
39
|
},
|
|
16
40
|
{
|
|
17
|
-
name: '
|
|
41
|
+
name: 'filesize',
|
|
18
42
|
type: 'number'
|
|
19
43
|
},
|
|
20
44
|
{
|
|
21
|
-
name: '
|
|
45
|
+
name: 'width',
|
|
22
46
|
type: 'number'
|
|
23
47
|
},
|
|
24
48
|
{
|
|
25
|
-
name: '
|
|
26
|
-
type: '
|
|
27
|
-
options: [
|
|
28
|
-
'pending',
|
|
29
|
-
'processing',
|
|
30
|
-
'complete',
|
|
31
|
-
'error'
|
|
32
|
-
]
|
|
49
|
+
name: 'height',
|
|
50
|
+
type: 'number'
|
|
33
51
|
},
|
|
34
52
|
{
|
|
35
|
-
name: '
|
|
53
|
+
name: 'mimeType',
|
|
36
54
|
type: 'text'
|
|
37
55
|
},
|
|
38
56
|
{
|
|
39
|
-
name: '
|
|
40
|
-
type: '
|
|
41
|
-
fields: [
|
|
42
|
-
{
|
|
43
|
-
name: 'format',
|
|
44
|
-
type: 'text'
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
name: 'filename',
|
|
48
|
-
type: 'text'
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
name: 'filesize',
|
|
52
|
-
type: 'number'
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
name: 'width',
|
|
56
|
-
type: 'number'
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: 'height',
|
|
60
|
-
type: 'number'
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: 'mimeType',
|
|
64
|
-
type: 'text'
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
name: 'url',
|
|
68
|
-
type: 'text'
|
|
69
|
-
}
|
|
70
|
-
]
|
|
57
|
+
name: 'url',
|
|
58
|
+
type: 'text'
|
|
71
59
|
}
|
|
72
60
|
]
|
|
61
|
+
}
|
|
62
|
+
];
|
|
63
|
+
export const getImageOptimizerField = (fieldsOverride)=>({
|
|
64
|
+
name: 'imageOptimizer',
|
|
65
|
+
type: 'group',
|
|
66
|
+
admin: {
|
|
67
|
+
position: 'sidebar',
|
|
68
|
+
readOnly: true,
|
|
69
|
+
components: {
|
|
70
|
+
Field: '@inoo-ch/payload-image-optimizer/client#OptimizationStatus'
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
fields: fieldsOverride ? fieldsOverride({
|
|
74
|
+
defaultFields: defaultImageOptimizerFields
|
|
75
|
+
}) : defaultImageOptimizerFields
|
|
73
76
|
});
|
|
74
77
|
|
|
75
78
|
//# sourceMappingURL=imageOptimizerField.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/fields/imageOptimizerField.ts"],"sourcesContent":["import type { GroupField } from 'payload'\n\
|
|
1
|
+
{"version":3,"sources":["../../src/fields/imageOptimizerField.ts"],"sourcesContent":["import type { Field, GroupField } from 'payload'\n\nimport type { FieldsOverride } from '../types.js'\n\nexport const defaultImageOptimizerFields: Field[] = [\n {\n name: 'thumbHash',\n type: 'text',\n },\n {\n name: 'originalSize',\n type: 'number',\n },\n {\n name: 'optimizedSize',\n type: 'number',\n },\n {\n name: 'status',\n type: 'select',\n options: ['pending', 'processing', 'complete', 'error'],\n },\n {\n name: 'error',\n type: 'text',\n },\n {\n name: 'variants',\n type: 'array',\n fields: [\n {\n name: 'format',\n type: 'text',\n },\n {\n name: 'filename',\n type: 'text',\n },\n {\n name: 'filesize',\n type: 'number',\n },\n {\n name: 'width',\n type: 'number',\n },\n {\n name: 'height',\n type: 'number',\n },\n {\n name: 'mimeType',\n type: 'text',\n },\n {\n name: 'url',\n type: 'text',\n },\n ],\n },\n]\n\nexport const getImageOptimizerField = (fieldsOverride?: FieldsOverride): GroupField => ({\n name: 'imageOptimizer',\n type: 'group',\n admin: {\n position: 'sidebar',\n readOnly: true,\n components: {\n Field: '@inoo-ch/payload-image-optimizer/client#OptimizationStatus',\n },\n },\n fields: fieldsOverride\n ? fieldsOverride({ defaultFields: defaultImageOptimizerFields })\n : defaultImageOptimizerFields,\n})\n"],"names":["defaultImageOptimizerFields","name","type","options","fields","getImageOptimizerField","fieldsOverride","admin","position","readOnly","components","Field","defaultFields"],"mappings":"AAIA,OAAO,MAAMA,8BAAuC;IAClD;QACEC,MAAM;QACNC,MAAM;IACR;IACA;QACED,MAAM;QACNC,MAAM;IACR;IACA;QACED,MAAM;QACNC,MAAM;IACR;IACA;QACED,MAAM;QACNC,MAAM;QACNC,SAAS;YAAC;YAAW;YAAc;YAAY;SAAQ;IACzD;IACA;QACEF,MAAM;QACNC,MAAM;IACR;IACA;QACED,MAAM;QACNC,MAAM;QACNE,QAAQ;YACN;gBACEH,MAAM;gBACNC,MAAM;YACR;YACA;gBACED,MAAM;gBACNC,MAAM;YACR;YACA;gBACED,MAAM;gBACNC,MAAM;YACR;YACA;gBACED,MAAM;gBACNC,MAAM;YACR;YACA;gBACED,MAAM;gBACNC,MAAM;YACR;YACA;gBACED,MAAM;gBACNC,MAAM;YACR;YACA;gBACED,MAAM;gBACNC,MAAM;YACR;SACD;IACH;CACD,CAAA;AAED,OAAO,MAAMG,yBAAyB,CAACC,iBAAiD,CAAA;QACtFL,MAAM;QACNC,MAAM;QACNK,OAAO;YACLC,UAAU;YACVC,UAAU;YACVC,YAAY;gBACVC,OAAO;YACT;QACF;QACAP,QAAQE,iBACJA,eAAe;YAAEM,eAAeZ;QAA4B,KAC5DA;IACN,CAAA,EAAE"}
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { resolveCollectionConfig } from '../defaults.js';
|
|
4
|
+
import { resolveStaticDir } from '../utilities/resolveStaticDir.js';
|
|
4
5
|
export const createAfterChangeHook = (resolvedConfig, collectionSlug)=>{
|
|
5
6
|
return async ({ context, doc, req })=>{
|
|
6
7
|
if (context?.imageOptimizer_skip) return doc;
|
|
7
8
|
if (!req.file || !req.file.data || !req.file.mimetype?.startsWith('image/')) return doc;
|
|
8
9
|
const collectionConfig = req.payload.collections[collectionSlug].config;
|
|
9
|
-
|
|
10
|
-
if (staticDir && !path.isAbsolute(staticDir)) {
|
|
11
|
-
staticDir = path.resolve(process.cwd(), staticDir);
|
|
12
|
-
}
|
|
10
|
+
const staticDir = resolveStaticDir(collectionConfig);
|
|
13
11
|
const perCollectionConfig = resolveCollectionConfig(resolvedConfig, collectionSlug);
|
|
14
12
|
// Overwrite the file on disk with the processed (stripped/resized/converted) buffer
|
|
15
13
|
// Payload 3.0 writes the original buffer to disk; we replace it here
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/afterChange.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\nimport type { CollectionAfterChangeHook } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\n\nexport const createAfterChangeHook = (\n resolvedConfig: ResolvedImageOptimizerConfig,\n collectionSlug: string,\n): CollectionAfterChangeHook => {\n return async ({ context, doc, req }) => {\n if (context?.imageOptimizer_skip) return doc\n\n if (!req.file || !req.file.data || !req.file.mimetype?.startsWith('image/')) return doc\n\n const collectionConfig = req.payload.collections[collectionSlug as keyof typeof req.payload.collections].config\n
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/afterChange.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\nimport type { CollectionAfterChangeHook } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { resolveStaticDir } from '../utilities/resolveStaticDir.js'\n\nexport const createAfterChangeHook = (\n resolvedConfig: ResolvedImageOptimizerConfig,\n collectionSlug: string,\n): CollectionAfterChangeHook => {\n return async ({ context, doc, req }) => {\n if (context?.imageOptimizer_skip) return doc\n\n if (!req.file || !req.file.data || !req.file.mimetype?.startsWith('image/')) return doc\n\n const collectionConfig = req.payload.collections[collectionSlug as keyof typeof req.payload.collections].config\n const staticDir = resolveStaticDir(collectionConfig)\n\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, collectionSlug)\n\n // Overwrite the file on disk with the processed (stripped/resized/converted) buffer\n // Payload 3.0 writes the original buffer to disk; we replace it here\n const processedBuffer = context.imageOptimizer_processedBuffer as Buffer | undefined\n if (processedBuffer && doc.filename && staticDir) {\n const safeFilename = path.basename(doc.filename as string)\n const filePath = path.join(staticDir, safeFilename)\n await fs.writeFile(filePath, processedBuffer)\n\n // If replaceOriginal changed the filename, clean up the old file Payload wrote\n const originalFilename = context.imageOptimizer_originalFilename as string | undefined\n if (originalFilename && originalFilename !== safeFilename) {\n const oldFilePath = path.join(staticDir, path.basename(originalFilename))\n await fs.unlink(oldFilePath).catch(() => {\n // Old file may not exist if Payload used the new filename\n })\n }\n }\n\n // When replaceOriginal is on and only one format is configured, the main file\n // is already converted — skip the async job and mark complete immediately.\n if (perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length <= 1) {\n await req.payload.update({\n collection: collectionSlug,\n id: doc.id,\n data: {\n imageOptimizer: {\n status: 'complete',\n variants: [],\n },\n },\n context: { imageOptimizer_skip: true },\n })\n return doc\n }\n\n // Queue async format conversion job for remaining variants\n await req.payload.jobs.queue({\n task: 'imageOptimizer_convertFormats',\n input: {\n collectionSlug,\n docId: String(doc.id),\n },\n })\n\n req.payload.jobs.run().catch((err: unknown) => {\n req.payload.logger.error({ err }, 'Image optimizer job runner failed')\n })\n\n return doc\n }\n}\n"],"names":["fs","path","resolveCollectionConfig","resolveStaticDir","createAfterChangeHook","resolvedConfig","collectionSlug","context","doc","req","imageOptimizer_skip","file","data","mimetype","startsWith","collectionConfig","payload","collections","config","staticDir","perCollectionConfig","processedBuffer","imageOptimizer_processedBuffer","filename","safeFilename","basename","filePath","join","writeFile","originalFilename","imageOptimizer_originalFilename","oldFilePath","unlink","catch","replaceOriginal","formats","length","update","collection","id","imageOptimizer","status","variants","jobs","queue","task","input","docId","String","run","err","logger","error"],"mappings":"AAAA,OAAOA,QAAQ,cAAa;AAC5B,OAAOC,UAAU,OAAM;AAIvB,SAASC,uBAAuB,QAAQ,iBAAgB;AACxD,SAASC,gBAAgB,QAAQ,mCAAkC;AAEnE,OAAO,MAAMC,wBAAwB,CACnCC,gBACAC;IAEA,OAAO,OAAO,EAAEC,OAAO,EAAEC,GAAG,EAAEC,GAAG,EAAE;QACjC,IAAIF,SAASG,qBAAqB,OAAOF;QAEzC,IAAI,CAACC,IAAIE,IAAI,IAAI,CAACF,IAAIE,IAAI,CAACC,IAAI,IAAI,CAACH,IAAIE,IAAI,CAACE,QAAQ,EAAEC,WAAW,WAAW,OAAON;QAEpF,MAAMO,mBAAmBN,IAAIO,OAAO,CAACC,WAAW,CAACX,eAAuD,CAACY,MAAM;QAC/G,MAAMC,YAAYhB,iBAAiBY;QAEnC,MAAMK,sBAAsBlB,wBAAwBG,gBAAgBC;QAEpE,oFAAoF;QACpF,qEAAqE;QACrE,MAAMe,kBAAkBd,QAAQe,8BAA8B;QAC9D,IAAID,mBAAmBb,IAAIe,QAAQ,IAAIJ,WAAW;YAChD,MAAMK,eAAevB,KAAKwB,QAAQ,CAACjB,IAAIe,QAAQ;YAC/C,MAAMG,WAAWzB,KAAK0B,IAAI,CAACR,WAAWK;YACtC,MAAMxB,GAAG4B,SAAS,CAACF,UAAUL;YAE7B,+EAA+E;YAC/E,MAAMQ,mBAAmBtB,QAAQuB,+BAA+B;YAChE,IAAID,oBAAoBA,qBAAqBL,cAAc;gBACzD,MAAMO,cAAc9B,KAAK0B,IAAI,CAACR,WAAWlB,KAAKwB,QAAQ,CAACI;gBACvD,MAAM7B,GAAGgC,MAAM,CAACD,aAAaE,KAAK,CAAC;gBACjC,0DAA0D;gBAC5D;YACF;QACF;QAEA,8EAA8E;QAC9E,2EAA2E;QAC3E,IAAIb,oBAAoBc,eAAe,IAAId,oBAAoBe,OAAO,CAACC,MAAM,IAAI,GAAG;YAClF,MAAM3B,IAAIO,OAAO,CAACqB,MAAM,CAAC;gBACvBC,YAAYhC;gBACZiC,IAAI/B,IAAI+B,EAAE;gBACV3B,MAAM;oBACJ4B,gBAAgB;wBACdC,QAAQ;wBACRC,UAAU,EAAE;oBACd;gBACF;gBACAnC,SAAS;oBAAEG,qBAAqB;gBAAK;YACvC;YACA,OAAOF;QACT;QAEA,2DAA2D;QAC3D,MAAMC,IAAIO,OAAO,CAAC2B,IAAI,CAACC,KAAK,CAAC;YAC3BC,MAAM;YACNC,OAAO;gBACLxC;gBACAyC,OAAOC,OAAOxC,IAAI+B,EAAE;YACtB;QACF;QAEA9B,IAAIO,OAAO,CAAC2B,IAAI,CAACM,GAAG,GAAGhB,KAAK,CAAC,CAACiB;YAC5BzC,IAAIO,OAAO,CAACmC,MAAM,CAACC,KAAK,CAAC;gBAAEF;YAAI,GAAG;QACpC;QAEA,OAAO1C;IACT;AACF,EAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Config } from 'payload';
|
|
2
2
|
import type { ImageOptimizerConfig } from './types.js';
|
|
3
|
-
export type { ImageOptimizerConfig, ImageFormat, FormatQuality, CollectionOptimizerConfig } from './types.js';
|
|
3
|
+
export type { ImageOptimizerConfig, ImageFormat, FormatQuality, CollectionOptimizerConfig, ImageOptimizerData, MediaResource, FieldsOverride } from './types.js';
|
|
4
|
+
export { defaultImageOptimizerFields } from './fields/imageOptimizerField.js';
|
|
4
5
|
export { encodeImageToThumbHash, decodeThumbHashToDataURL } from './utilities/thumbhash.js';
|
|
5
6
|
export declare const imageOptimizer: (pluginOptions: ImageOptimizerConfig) => (config: Config) => Config;
|