feat: Add new emoji assets and an UpdateBanner component.
Some checks failed
Build and Release / build-and-release (push) Failing after 3m28s
Some checks failed
Build and Release / build-and-release (push) Failing after 3m28s
This commit is contained in:
134
packages/shared/src/components/AvatarCropModal.jsx
Normal file
134
packages/shared/src/components/AvatarCropModal.jsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import Cropper from 'react-easy-crop';
|
||||
|
||||
function getCroppedImg(imageSrc, pixelCrop) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.crossOrigin = 'anonymous';
|
||||
image.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 256;
|
||||
canvas.height = 256;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(
|
||||
image,
|
||||
pixelCrop.x, pixelCrop.y, pixelCrop.width, pixelCrop.height,
|
||||
0, 0, 256, 256
|
||||
);
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) return reject(new Error('Canvas toBlob failed'));
|
||||
resolve(blob);
|
||||
}, 'image/png');
|
||||
};
|
||||
image.onerror = reject;
|
||||
image.src = imageSrc;
|
||||
});
|
||||
}
|
||||
|
||||
const AvatarCropModal = ({ imageUrl, onApply, onCancel, cropShape = 'round' }) => {
|
||||
const [crop, setCrop] = useState({ x: 0, y: 0 });
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [croppedAreaPixels, setCroppedAreaPixels] = useState(null);
|
||||
|
||||
const onCropComplete = useCallback((_croppedArea, croppedPixels) => {
|
||||
setCroppedAreaPixels(croppedPixels);
|
||||
}, []);
|
||||
|
||||
const handleApply = useCallback(async () => {
|
||||
if (!croppedAreaPixels) return;
|
||||
const blob = await getCroppedImg(imageUrl, croppedAreaPixels);
|
||||
onApply(blob);
|
||||
}, [imageUrl, croppedAreaPixels, onApply]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKey = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.stopPropagation();
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKey, true);
|
||||
return () => window.removeEventListener('keydown', handleKey, true);
|
||||
}, [onCancel]);
|
||||
|
||||
return (
|
||||
<div className="avatar-crop-overlay" onMouseDown={(e) => { if (e.target === e.currentTarget) onCancel(); }}>
|
||||
<div className="avatar-crop-dialog">
|
||||
{/* Header */}
|
||||
<div className="avatar-crop-header">
|
||||
<h2 style={{ margin: 0, fontSize: '20px', fontWeight: 700, color: 'var(--header-primary)' }}>
|
||||
Edit Image
|
||||
</h2>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
style={{
|
||||
background: 'none', border: 'none', color: 'var(--header-secondary)',
|
||||
fontSize: '24px', cursor: 'pointer', padding: '4px', lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Crop area */}
|
||||
<div className="avatar-crop-area">
|
||||
<Cropper
|
||||
image={imageUrl}
|
||||
crop={crop}
|
||||
zoom={zoom}
|
||||
aspect={1}
|
||||
cropShape={cropShape}
|
||||
showGrid={false}
|
||||
onCropChange={setCrop}
|
||||
onZoomChange={setZoom}
|
||||
onCropComplete={onCropComplete}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Zoom slider */}
|
||||
<div className="avatar-crop-slider-row">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="var(--header-secondary)">
|
||||
<path d="M15 3H9v2h6V3zm-4 18h2v-2h-2v2zm8-15.97l-1.41-1.41-1.76 1.76 1.41 1.41L19 5.03zM4.76 4.38L3.34 5.8 5.1 7.56 6.52 6.14 4.76 4.38zM21 11h-2v2h2v-2zM5 11H3v2h2v-2zm7-4a5 5 0 100 10 5 5 0 000-10z"/>
|
||||
</svg>
|
||||
<input
|
||||
type="range"
|
||||
min={1}
|
||||
max={3}
|
||||
step={0.01}
|
||||
value={zoom}
|
||||
onChange={(e) => setZoom(Number(e.target.value))}
|
||||
className="avatar-crop-slider"
|
||||
/>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="var(--header-secondary)">
|
||||
<path d="M15 3H9v2h6V3zm-4 18h2v-2h-2v2zm8-15.97l-1.41-1.41-1.76 1.76 1.41 1.41L19 5.03zM4.76 4.38L3.34 5.8 5.1 7.56 6.52 6.14 4.76 4.38zM21 11h-2v2h2v-2zM5 11H3v2h2v-2zm7-4a5 5 0 100 10 5 5 0 000-10z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="avatar-crop-actions">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
style={{
|
||||
background: 'none', border: 'none', color: 'var(--header-primary)',
|
||||
cursor: 'pointer', fontSize: '14px', fontWeight: 500, padding: '8px 16px',
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleApply}
|
||||
style={{
|
||||
backgroundColor: 'var(--brand-experiment)', color: 'white', border: 'none',
|
||||
borderRadius: '4px', padding: '8px 24px', cursor: 'pointer',
|
||||
fontSize: '14px', fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarCropModal;
|
||||
Reference in New Issue
Block a user