CropModals: two different for now because of aspect ratios - maybe merge into one
This commit is contained in:
parent
20c71636f6
commit
1c87ba150e
@ -0,0 +1,132 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useState, useCallback } from 'react'
|
||||||
|
import Cropper from 'react-easy-crop'
|
||||||
|
import { Point, Area } from 'react-easy-crop'
|
||||||
|
|
||||||
|
interface AffiliateCropModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
imageSrc: string
|
||||||
|
onClose: () => void
|
||||||
|
onCropComplete: (croppedImageBlob: Blob) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AffiliateCropModal({ isOpen, imageSrc, onClose, onCropComplete }: AffiliateCropModalProps) {
|
||||||
|
const [crop, setCrop] = useState<Point>({ x: 0, y: 0 })
|
||||||
|
const [zoom, setZoom] = useState(1)
|
||||||
|
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null)
|
||||||
|
|
||||||
|
const onCropAreaComplete = useCallback((_croppedArea: Area, croppedAreaPixels: Area) => {
|
||||||
|
setCroppedAreaPixels(croppedAreaPixels)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const createCroppedImage = async () => {
|
||||||
|
if (!croppedAreaPixels) return
|
||||||
|
|
||||||
|
const image = new Image()
|
||||||
|
image.src = imageSrc
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
image.onload = resolve
|
||||||
|
})
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
// Set canvas size to cropped area
|
||||||
|
canvas.width = croppedAreaPixels.width
|
||||||
|
canvas.height = croppedAreaPixels.height
|
||||||
|
|
||||||
|
ctx.drawImage(
|
||||||
|
image,
|
||||||
|
croppedAreaPixels.x,
|
||||||
|
croppedAreaPixels.y,
|
||||||
|
croppedAreaPixels.width,
|
||||||
|
croppedAreaPixels.height,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
croppedAreaPixels.width,
|
||||||
|
croppedAreaPixels.height
|
||||||
|
)
|
||||||
|
|
||||||
|
return new Promise<Blob>((resolve) => {
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (blob) resolve(blob)
|
||||||
|
}, 'image/jpeg', 0.95)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
const croppedBlob = await createCroppedImage()
|
||||||
|
if (croppedBlob) {
|
||||||
|
onCropComplete(croppedBlob)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOpen) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/70">
|
||||||
|
<div className="relative w-full max-w-4xl mx-4 bg-white rounded-2xl shadow-2xl overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between bg-gradient-to-r from-blue-50 to-white">
|
||||||
|
<h2 className="text-xl font-semibold text-blue-900">Crop Affiliate Logo</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-500 hover:text-gray-700 transition"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Crop Area */}
|
||||||
|
<div className="relative bg-gray-900" style={{ height: '500px' }}>
|
||||||
|
<Cropper
|
||||||
|
image={imageSrc}
|
||||||
|
crop={crop}
|
||||||
|
zoom={zoom}
|
||||||
|
aspect={3 / 2}
|
||||||
|
onCropChange={setCrop}
|
||||||
|
onZoomChange={setZoom}
|
||||||
|
onCropComplete={onCropAreaComplete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Controls */}
|
||||||
|
<div className="px-6 py-4 border-t border-gray-200 bg-gray-50">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-blue-900 mb-2">
|
||||||
|
Zoom: {zoom.toFixed(1)}x
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={1}
|
||||||
|
max={3}
|
||||||
|
step={0.1}
|
||||||
|
value={zoom}
|
||||||
|
onChange={(e) => setZoom(Number(e.target.value))}
|
||||||
|
className="w-full h-2 bg-blue-200 rounded-lg appearance-none cursor-pointer accent-blue-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-5 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="px-5 py-2.5 text-sm font-semibold text-white bg-blue-900 rounded-lg hover:bg-blue-800 shadow transition"
|
||||||
|
>
|
||||||
|
Apply Crop
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
132
src/app/admin/subscriptions/components/ImageCropModal.tsx
Normal file
132
src/app/admin/subscriptions/components/ImageCropModal.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useState, useCallback } from 'react'
|
||||||
|
import Cropper from 'react-easy-crop'
|
||||||
|
import { Point, Area } from 'react-easy-crop'
|
||||||
|
|
||||||
|
interface ImageCropModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
imageSrc: string
|
||||||
|
onClose: () => void
|
||||||
|
onCropComplete: (croppedImageBlob: Blob) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ImageCropModal({ isOpen, imageSrc, onClose, onCropComplete }: ImageCropModalProps) {
|
||||||
|
const [crop, setCrop] = useState<Point>({ x: 0, y: 0 })
|
||||||
|
const [zoom, setZoom] = useState(1)
|
||||||
|
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null)
|
||||||
|
|
||||||
|
const onCropAreaComplete = useCallback((_croppedArea: Area, croppedAreaPixels: Area) => {
|
||||||
|
setCroppedAreaPixels(croppedAreaPixels)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const createCroppedImage = async () => {
|
||||||
|
if (!croppedAreaPixels) return
|
||||||
|
|
||||||
|
const image = new Image()
|
||||||
|
image.src = imageSrc
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
image.onload = resolve
|
||||||
|
})
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
// Set canvas size to cropped area
|
||||||
|
canvas.width = croppedAreaPixels.width
|
||||||
|
canvas.height = croppedAreaPixels.height
|
||||||
|
|
||||||
|
ctx.drawImage(
|
||||||
|
image,
|
||||||
|
croppedAreaPixels.x,
|
||||||
|
croppedAreaPixels.y,
|
||||||
|
croppedAreaPixels.width,
|
||||||
|
croppedAreaPixels.height,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
croppedAreaPixels.width,
|
||||||
|
croppedAreaPixels.height
|
||||||
|
)
|
||||||
|
|
||||||
|
return new Promise<Blob>((resolve) => {
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (blob) resolve(blob)
|
||||||
|
}, 'image/jpeg', 0.95)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
const croppedBlob = await createCroppedImage()
|
||||||
|
if (croppedBlob) {
|
||||||
|
onCropComplete(croppedBlob)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOpen) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70">
|
||||||
|
<div className="relative w-full max-w-4xl mx-4 bg-white rounded-2xl shadow-2xl overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between bg-gradient-to-r from-blue-50 to-white">
|
||||||
|
<h2 className="text-xl font-semibold text-blue-900">Crop & Adjust Image</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-500 hover:text-gray-700 transition"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Crop Area */}
|
||||||
|
<div className="relative bg-gray-900" style={{ height: '500px' }}>
|
||||||
|
<Cropper
|
||||||
|
image={imageSrc}
|
||||||
|
crop={crop}
|
||||||
|
zoom={zoom}
|
||||||
|
aspect={16 / 9}
|
||||||
|
onCropChange={setCrop}
|
||||||
|
onZoomChange={setZoom}
|
||||||
|
onCropComplete={onCropAreaComplete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Controls */}
|
||||||
|
<div className="px-6 py-4 border-t border-gray-200 bg-gray-50">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-blue-900 mb-2">
|
||||||
|
Zoom: {zoom.toFixed(1)}x
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={1}
|
||||||
|
max={3}
|
||||||
|
step={0.1}
|
||||||
|
value={zoom}
|
||||||
|
onChange={(e) => setZoom(Number(e.target.value))}
|
||||||
|
className="w-full h-2 bg-blue-200 rounded-lg appearance-none cursor-pointer accent-blue-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-5 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="px-5 py-2.5 text-sm font-semibold text-white bg-blue-900 rounded-lg hover:bg-blue-800 shadow transition"
|
||||||
|
>
|
||||||
|
Apply Crop
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user