Initial commit: Meshkee dashboards monorepo.
Includes business, customer, and super-admin apps with shared packages and production deploy scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import Cropper, { type Area } from 'react-easy-crop'
|
||||
import { ImagePlus, X } from 'lucide-react'
|
||||
import { getCroppedImage } from '../utils/cropImage'
|
||||
import styles from './ImageCropper.module.css'
|
||||
|
||||
interface ImageCropperProps {
|
||||
value: string | null
|
||||
onChange: (value: string | null) => void
|
||||
aspect?: number
|
||||
outputFormat?: 'jpeg' | 'png'
|
||||
accept?: string
|
||||
uploadLabel?: string
|
||||
hint?: string
|
||||
changeLabel?: string
|
||||
}
|
||||
|
||||
export function ImageCropper({
|
||||
value,
|
||||
onChange,
|
||||
aspect = 1,
|
||||
outputFormat = 'jpeg',
|
||||
accept = 'image/*',
|
||||
uploadLabel = 'Upload thumbnail image',
|
||||
hint = 'Click to select, then crop',
|
||||
changeLabel = 'Change thumbnail',
|
||||
}: ImageCropperProps) {
|
||||
const [imageSrc, setImageSrc] = useState<string | null>(null)
|
||||
const [crop, setCrop] = useState({ x: 0, y: 0 })
|
||||
const [zoom, setZoom] = useState(1)
|
||||
const [croppedArea, setCroppedArea] = useState<Area | null>(null)
|
||||
|
||||
const onCropComplete = useCallback((_: Area, pixels: Area) => {
|
||||
setCroppedArea(pixels)
|
||||
}, [])
|
||||
|
||||
function handleFile(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => setImageSrc(reader.result as string)
|
||||
reader.readAsDataURL(file)
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
async function applyCrop() {
|
||||
if (!imageSrc || !croppedArea) return
|
||||
const cropped = await getCroppedImage(imageSrc, croppedArea, outputFormat)
|
||||
onChange(cropped)
|
||||
setImageSrc(null)
|
||||
setZoom(1)
|
||||
setCrop({ x: 0, y: 0 })
|
||||
}
|
||||
|
||||
function cancelCrop() {
|
||||
setImageSrc(null)
|
||||
setZoom(1)
|
||||
setCrop({ x: 0, y: 0 })
|
||||
}
|
||||
|
||||
function removeThumbnail() {
|
||||
onChange(null)
|
||||
}
|
||||
|
||||
const frameStyle = { aspectRatio: `${aspect}` as const }
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{value && !imageSrc && (
|
||||
<div className={styles.preview} style={frameStyle}>
|
||||
<img src={value} alt="Thumbnail preview" className={styles.previewImg} />
|
||||
<button type="button" className={styles.removeBtn} onClick={removeThumbnail} aria-label="Remove thumbnail">
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!value && !imageSrc && (
|
||||
<label className={styles.uploadZone} style={frameStyle}>
|
||||
<ImagePlus size={28} />
|
||||
<span>{uploadLabel}</span>
|
||||
<span className={styles.hint}>{hint}</span>
|
||||
<input type="file" accept={accept} onChange={handleFile} hidden />
|
||||
</label>
|
||||
)}
|
||||
|
||||
{imageSrc && (
|
||||
<div className={styles.cropPanel}>
|
||||
<div className={styles.cropArea} style={frameStyle}>
|
||||
<Cropper
|
||||
image={imageSrc}
|
||||
crop={crop}
|
||||
zoom={zoom}
|
||||
aspect={aspect}
|
||||
onCropChange={setCrop}
|
||||
onZoomChange={setZoom}
|
||||
onCropComplete={onCropComplete}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.cropControls}>
|
||||
<label className={styles.zoomLabel}>
|
||||
Zoom
|
||||
<input
|
||||
type="range"
|
||||
min={1}
|
||||
max={3}
|
||||
step={0.05}
|
||||
value={zoom}
|
||||
onChange={(e) => setZoom(Number(e.target.value))}
|
||||
/>
|
||||
</label>
|
||||
<div className={styles.cropActions}>
|
||||
<button type="button" className={styles.cancelBtn} onClick={cancelCrop}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" className={styles.applyBtn} onClick={applyCrop}>
|
||||
Apply Crop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{value && !imageSrc && (
|
||||
<label className={styles.changeBtn}>
|
||||
{changeLabel}
|
||||
<input type="file" accept={accept} onChange={handleFile} hidden />
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user