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