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:
Alireza Hassani
2026-09-24 23:17:51 +03:30
co-authored by Cursor
parent ff419603bf
commit e2950fa20e
26 changed files with 2661 additions and 37 deletions
+39 -13
View File
@@ -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>