Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .Jules/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
## [Unreleased]

### Added
- **Image Upload Cropper:** Added ability to preview and crop profile pictures before uploading.
- **Features:**
- Interactive circular cropper using `react-image-crop`.
- Integrated with existing Profile editing modal.
- Dual-theme support (Glassmorphism & Neobrutalism).
- **Technical:** Created `web/components/ui/ImageCropper.tsx`. Added `react-image-crop` dependency. Integrated into `web/pages/Profile.tsx`.

- **Password Strength Meter:** Added a visual password strength indicator to the signup form.
- **Features:**
- Real-time strength calculation (Length, Uppercase, Lowercase, Number, Symbol).
Expand Down
5 changes: 5 additions & 0 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,8 @@
- Completed: 2026-02-08
- Files modified: `web/components/ui/PasswordStrength.tsx`, `web/pages/Auth.tsx`
- Impact: Provides visual feedback on password complexity during signup

- [x] **[ux]** Image upload with preview and crop
- Completed: 2026-02-23
- Files modified: `web/components/ui/ImageCropper.tsx`, `web/pages/Profile.tsx`
- Impact: Professional avatar upload experience with cropping
4 changes: 4 additions & 0 deletions test-results/.last-run.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"status": "failed",
"failedTests": []
}
127 changes: 127 additions & 0 deletions web/components/ui/ImageCropper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import React, { useState, useRef } from 'react';
import ReactCrop, { Crop, PixelCrop, centerCrop, makeAspectCrop } from 'react-image-crop';
import 'react-image-crop/dist/ReactCrop.css';
import { Button } from './Button';
import { Modal } from './Modal';
import { useTheme } from '../../contexts/ThemeContext';
import { THEMES } from '../../constants';

interface ImageCropperProps {
isOpen: boolean;
imageUrl: string;
onClose: () => void;
onCropComplete: (croppedBase64: string) => void;
}

function centerAspectCrop(
mediaWidth: number,
mediaHeight: number,
aspect: number,
) {
return centerCrop(
makeAspectCrop(
{
unit: '%',
width: 90,
},
aspect,
mediaWidth,
mediaHeight,
),
mediaWidth,
mediaHeight,
)
}

export const ImageCropper: React.FC<ImageCropperProps> = ({
isOpen,
imageUrl,
onClose,
onCropComplete
}) => {
const { style } = useTheme();
const isNeo = style === THEMES.NEOBRUTALISM;
const [crop, setCrop] = useState<Crop>();
const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
const imgRef = useRef<HTMLImageElement>(null);

const onImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
const { width, height } = e.currentTarget;
setCrop(centerAspectCrop(width, height, 1));
};

const handleSave = () => {
if (!completedCrop || !imgRef.current) {
return;
}
Comment on lines +54 to +56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against zero-dimension crops before enabling/applying save.

completedCrop can be truthy while width/height are zero in edge cases; this can emit an invalid/empty data URL.

Proposed patch
-        if (!completedCrop || !imgRef.current) {
+        if (!completedCrop || completedCrop.width <= 0 || completedCrop.height <= 0 || !imgRef.current) {
             return;
         }
...
-                    <Button onClick={handleSave} disabled={!completedCrop}>
+                    <Button onClick={handleSave} disabled={!completedCrop || completedCrop.width <= 0 || completedCrop.height <= 0}>
                         Apply
                     </Button>

Also applies to: 97-99

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/components/ui/ImageCropper.tsx` around lines 54 - 56, The current guard
(checking only completedCrop and imgRef.current) can pass when completedCrop has
zero width/height and produce an empty data URL; update both checks that
currently read "if (!completedCrop || !imgRef.current) return;" to also validate
completedCrop.width > 0 && completedCrop.height > 0 (e.g., "if (!completedCrop
|| !imgRef.current || completedCrop.width <= 0 || completedCrop.height <= 0)
return;") so functions that use completedCrop (the crop-handling logic and the
save/export path referenced in the file) bail out on zero-dimension crops.


const canvas = document.createElement('canvas');
const scaleX = imgRef.current.naturalWidth / imgRef.current.width;
const scaleY = imgRef.current.naturalHeight / imgRef.current.height;

canvas.width = completedCrop.width;
canvas.height = completedCrop.height;

const ctx = canvas.getContext('2d');
if (!ctx) {
return;
}

ctx.drawImage(
imgRef.current,
completedCrop.x * scaleX,
completedCrop.y * scaleY,
completedCrop.width * scaleX,
completedCrop.height * scaleY,
0,
0,
completedCrop.width,
completedCrop.height
);

const base64Image = canvas.toDataURL('image/jpeg', 0.9);
onCropComplete(base64Image);
onClose();
};

return (
<Modal
isOpen={isOpen}
onClose={onClose}
title="Crop Image"
footer={
<>
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
<Button onClick={handleSave} disabled={!completedCrop}>
Apply
</Button>
</>
}
>
<div className="flex flex-col items-center justify-center p-4">
{imageUrl && (
<div className={`max-h-[60vh] overflow-hidden ${isNeo ? 'border-4 border-black' : 'rounded-lg overflow-hidden border border-white/20'}`}>
<ReactCrop
crop={crop}
onChange={(_, percentCrop) => setCrop(percentCrop)}
onComplete={(c) => setCompletedCrop(c)}
aspect={1}
circularCrop
>
<img
ref={imgRef}
src={imageUrl}
onLoad={onImageLoad}
alt="Crop me"
className="max-w-full max-h-[50vh] object-contain"
/>
</ReactCrop>
</div>
)}
<p className="mt-4 text-sm opacity-60">Drag to adjust the circular crop area</p>
</div>
</Modal>
);
};
Loading
Loading