Add store installments module with calculator and public API docs.
Business dashboard can manage credit methods and preview plans; cropper and module flags support the new flow. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
ff419603bf
commit
e2950fa20e
@@ -10,6 +10,10 @@ interface ImageCropperProps {
|
||||
onChange: (value: string | null) => void
|
||||
/** Crop aspect ratio. Defaults to square (1). */
|
||||
aspect?: number
|
||||
/**
|
||||
* Output format. When omitted, follows the uploaded file
|
||||
* (PNG stays PNG with transparent padding; other images become JPEG with white padding).
|
||||
*/
|
||||
outputFormat?: 'jpeg' | 'png'
|
||||
/** Fill color for JPEG and for crop areas outside the image. */
|
||||
fillColor?: string
|
||||
@@ -24,13 +28,24 @@ interface ImageCropperProps {
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
function isPngSource(file: File): boolean {
|
||||
return (
|
||||
file.type === 'image/png' ||
|
||||
file.name.toLowerCase().endsWith('.png')
|
||||
)
|
||||
}
|
||||
|
||||
function isPngDataUrl(src: string): boolean {
|
||||
return /^data:image\/png/i.test(src)
|
||||
}
|
||||
|
||||
export function ImageCropper({
|
||||
value,
|
||||
onChange,
|
||||
aspect = 1,
|
||||
outputFormat = 'jpeg',
|
||||
outputFormat,
|
||||
fillColor,
|
||||
minZoom = 1,
|
||||
minZoom = 0.4,
|
||||
maxZoom = 3,
|
||||
accept = 'image/*',
|
||||
uploadLabel,
|
||||
@@ -42,13 +57,18 @@ export function ImageCropper({
|
||||
const resolvedUploadLabel = uploadLabel ?? t('products.form.thumbnailUpload')
|
||||
const resolvedHint = hint ?? t('products.form.thumbnailHint')
|
||||
const resolvedChangeLabel = changeLabel ?? t('products.form.thumbnailChange')
|
||||
const resolvedFillColor = fillColor ?? (outputFormat === 'jpeg' ? '#ffffff' : undefined)
|
||||
const cropSurfaceColor = resolvedFillColor ?? '#1e293b'
|
||||
const [imageSrc, setImageSrc] = useState<string | null>(null)
|
||||
const [sourceIsPng, setSourceIsPng] = useState(false)
|
||||
const [crop, setCrop] = useState({ x: 0, y: 0 })
|
||||
const [zoom, setZoom] = useState(1)
|
||||
const [croppedArea, setCroppedArea] = useState<Area | null>(null)
|
||||
|
||||
const resolvedFormat: 'jpeg' | 'png' =
|
||||
outputFormat ?? (sourceIsPng ? 'png' : 'jpeg')
|
||||
const resolvedFillColor =
|
||||
fillColor ?? (resolvedFormat === 'jpeg' ? '#ffffff' : undefined)
|
||||
const useTransparentPad = resolvedFormat === 'png' && !resolvedFillColor
|
||||
|
||||
useEffect(() => {
|
||||
setCrop({ x: 0, y: 0 })
|
||||
setZoom(1)
|
||||
@@ -62,6 +82,8 @@ export function ImageCropper({
|
||||
function handleFile(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
const png = isPngSource(file)
|
||||
setSourceIsPng(png)
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
setImageSrc(reader.result as string)
|
||||
@@ -75,12 +97,10 @@ export function ImageCropper({
|
||||
|
||||
async function applyCrop() {
|
||||
if (!imageSrc || !croppedArea) return
|
||||
const cropped = await getCroppedImage(
|
||||
imageSrc,
|
||||
croppedArea,
|
||||
outputFormat,
|
||||
resolvedFillColor,
|
||||
)
|
||||
const format: 'jpeg' | 'png' =
|
||||
outputFormat ?? (isPngDataUrl(imageSrc) || sourceIsPng ? 'png' : 'jpeg')
|
||||
const fill = fillColor ?? (format === 'jpeg' ? '#ffffff' : undefined)
|
||||
const cropped = await getCroppedImage(imageSrc, croppedArea, format, fill)
|
||||
onChange(cropped)
|
||||
setImageSrc(null)
|
||||
setZoom(1)
|
||||
@@ -116,6 +136,10 @@ export function ImageCropper({
|
||||
alignSelf: 'center',
|
||||
}
|
||||
|
||||
const cropSurfaceStyle: React.CSSProperties = useTransparentPad
|
||||
? { ...frameStyle }
|
||||
: { ...frameStyle, background: resolvedFillColor ?? '#1e293b' }
|
||||
|
||||
return (
|
||||
<div className={`${styles.wrapper} ${compact ? styles.compact : ''}`}>
|
||||
{value && !imageSrc && (
|
||||
@@ -144,8 +168,8 @@ export function ImageCropper({
|
||||
{imageSrc && (
|
||||
<div className={styles.cropPanel}>
|
||||
<div
|
||||
className={styles.cropArea}
|
||||
style={{ ...frameStyle, background: cropSurfaceColor }}
|
||||
className={`${styles.cropArea} ${useTransparentPad ? styles.cropAreaTransparent : ''}`}
|
||||
style={cropSurfaceStyle}
|
||||
>
|
||||
<Cropper
|
||||
key={String(aspect)}
|
||||
@@ -160,7 +184,9 @@ export function ImageCropper({
|
||||
onCropComplete={onCropComplete}
|
||||
restrictPosition={minZoom >= 1}
|
||||
style={{
|
||||
containerStyle: { background: cropSurfaceColor },
|
||||
containerStyle: useTransparentPad
|
||||
? { background: 'transparent' }
|
||||
: { background: resolvedFillColor ?? '#1e293b' },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user