2026-01-07 09:31:02 +00:00
|
|
|
import React, { useState, useCallback, lazy, Suspense } from 'react';
|
|
|
|
|
// PERF: Lazy load react-easy-crop (composant volumineux ~100KB)
|
2026-01-07 18:39:21 +00:00
|
|
|
// Mock Cropper since react-easy-crop is missing
|
2026-01-13 18:47:57 +00:00
|
|
|
const Cropper = lazy(() =>
|
|
|
|
|
Promise.resolve({
|
|
|
|
|
default: (_props: any) => (
|
2026-01-16 00:56:37 +00:00
|
|
|
<div className="bg-kodo-graphite flex items-center justify-center h-full text-white">
|
2026-01-13 18:47:57 +00:00
|
|
|
Cropper Mock
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
}),
|
|
|
|
|
);
|
2026-01-07 18:39:21 +00:00
|
|
|
import { Button } from '@/components/ui/button';
|
2026-01-07 09:31:02 +00:00
|
|
|
import { X, ZoomIn, RotateCw, Check } from 'lucide-react';
|
|
|
|
|
import { LoadingSpinner } from './loading-spinner';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ImageCropperProps - Propriétés du composant ImageCropper
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @interface ImageCropperProps
|
|
|
|
|
*/
|
|
|
|
|
interface ImageCropperProps {
|
|
|
|
|
/**
|
|
|
|
|
* URL ou source de l'image à recadrer
|
|
|
|
|
*/
|
|
|
|
|
imageSrc: string;
|
2026-01-07 18:39:21 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* Ratio d'aspect du recadrage
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* - `1`: Ratio carré (pour avatars)
|
|
|
|
|
* - `3`: Ratio paysage (pour bannières)
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @example
|
|
|
|
|
* ```tsx
|
|
|
|
|
* <ImageCropper imageSrc={image} aspectRatio={1} />
|
|
|
|
|
* ```
|
|
|
|
|
*/
|
|
|
|
|
aspectRatio: number;
|
2026-01-07 18:39:21 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* Fonction appelée pour annuler le recadrage
|
|
|
|
|
*/
|
|
|
|
|
onCancel: () => void;
|
2026-01-07 18:39:21 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* Fonction appelée lorsque le recadrage est terminé
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @param {any} croppedAreaPixels - Zone recadrée en pixels
|
|
|
|
|
*/
|
|
|
|
|
onCropComplete: (croppedAreaPixels: any) => void;
|
2026-01-07 18:39:21 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* Si `true`, utilise un recadrage circulaire (pour avatars)
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @default false
|
|
|
|
|
*/
|
|
|
|
|
circularCrop?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ImageCropper - Composant de recadrage d'image avec design system Kodo
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* Composant modal pour recadrer des images avec support pour :
|
|
|
|
|
* - Zoom (1x à 3x)
|
|
|
|
|
* - Rotation (0° à 360°)
|
|
|
|
|
* - Recadrage circulaire ou rectangulaire
|
|
|
|
|
* - Ratio d'aspect personnalisable
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @example
|
|
|
|
|
* ```tsx
|
|
|
|
|
* // Recadrage d'avatar (carré, circulaire)
|
|
|
|
|
* <ImageCropper
|
|
|
|
|
* imageSrc={avatarSrc}
|
|
|
|
|
* aspectRatio={1}
|
|
|
|
|
* circularCrop={true}
|
|
|
|
|
* onCancel={() => setShowCropper(false)}
|
|
|
|
|
* onCropComplete={(area) => handleCrop(area)}
|
|
|
|
|
* />
|
|
|
|
|
* ```
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @example
|
|
|
|
|
* ```tsx
|
|
|
|
|
* // Recadrage de bannière (paysage, rectangulaire)
|
|
|
|
|
* <ImageCropper
|
|
|
|
|
* imageSrc={bannerSrc}
|
|
|
|
|
* aspectRatio={3}
|
|
|
|
|
* circularCrop={false}
|
|
|
|
|
* onCancel={() => setShowCropper(false)}
|
|
|
|
|
* onCropComplete={(area) => handleCrop(area)}
|
|
|
|
|
* />
|
|
|
|
|
* ```
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @component
|
|
|
|
|
* @param {ImageCropperProps} props - Propriétés du composant
|
|
|
|
|
* @returns {JSX.Element} Modal de recadrage avec contrôles
|
|
|
|
|
*/
|
|
|
|
|
|
2026-01-07 18:39:21 +00:00
|
|
|
export const ImageCropper: React.FC<ImageCropperProps> = ({
|
|
|
|
|
imageSrc,
|
|
|
|
|
aspectRatio,
|
|
|
|
|
onCancel,
|
|
|
|
|
onCropComplete,
|
2026-01-13 18:47:57 +00:00
|
|
|
circularCrop = false,
|
2026-01-07 09:31:02 +00:00
|
|
|
}) => {
|
|
|
|
|
const [crop, setCrop] = useState({ x: 0, y: 0 });
|
|
|
|
|
const [zoom, setZoom] = useState(1);
|
|
|
|
|
const [rotation, setRotation] = useState(0);
|
|
|
|
|
const [croppedAreaPixels, setCroppedAreaPixels] = useState(null);
|
|
|
|
|
|
|
|
|
|
const onCropChange = (crop: { x: number; y: number }) => {
|
|
|
|
|
setCrop(crop);
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
const onCropCompleteHandler = useCallback(
|
|
|
|
|
(_croppedArea: any, croppedAreaPixels: any) => {
|
|
|
|
|
setCroppedAreaPixels(croppedAreaPixels);
|
|
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
);
|
2026-01-07 09:31:02 +00:00
|
|
|
|
|
|
|
|
const handleSave = () => {
|
|
|
|
|
onCropComplete(croppedAreaPixels);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-kodo-void/95 backdrop-blur-sm">
|
|
|
|
|
<div className="relative w-full max-w-2xl bg-kodo-graphite border border-kodo-steel rounded-xl shadow-2xl overflow-hidden flex flex-col h-[80vh]">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="p-4 border-b border-kodo-steel flex justify-between items-center bg-kodo-ink">
|
|
|
|
|
<h3 className="font-bold text-white flex items-center gap-2">
|
|
|
|
|
Edit Image
|
|
|
|
|
</h3>
|
consistency: replace custom buttons with Button component (partial)
- Replaced custom button implementations with Button component in 14 files
- Files updated: LiveStreamDetailView, DashboardPage, CommentItem, PostCard, SocialPage, SocialView, AdminUsersView, UserTableRow, ProjectsManager, CloudFileBrowser, FileManagerView, CreatorModal, ImageCropper, BulkUploadModal
- ~31 buttons replaced across high-priority files
- Used appropriate Button variants: ghost, outline, default, secondary, link
- Preserved visual appearance with className overrides where needed
- Action 9.2.1.2 in progress (partial completion)
2026-01-16 01:06:14 +00:00
|
|
|
<Button variant="ghost" size="icon" onClick={onCancel}>
|
2026-01-13 18:47:57 +00:00
|
|
|
<X className="w-5 h-5" />
|
consistency: replace custom buttons with Button component (partial)
- Replaced custom button implementations with Button component in 14 files
- Files updated: LiveStreamDetailView, DashboardPage, CommentItem, PostCard, SocialPage, SocialView, AdminUsersView, UserTableRow, ProjectsManager, CloudFileBrowser, FileManagerView, CreatorModal, ImageCropper, BulkUploadModal
- ~31 buttons replaced across high-priority files
- Used appropriate Button variants: ghost, outline, default, secondary, link
- Preserved visual appearance with className overrides where needed
- Action 9.2.1.2 in progress (partial completion)
2026-01-16 01:06:14 +00:00
|
|
|
</Button>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Cropper Area */}
|
|
|
|
|
<div className="relative flex-1 bg-black">
|
2026-01-13 18:47:57 +00:00
|
|
|
<Suspense
|
|
|
|
|
fallback={
|
|
|
|
|
<div className="w-full h-full flex items-center justify-center">
|
|
|
|
|
<LoadingSpinner size="lg" text="Chargement du recadreur..." />
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
>
|
2026-01-07 09:31:02 +00:00
|
|
|
<Cropper
|
|
|
|
|
image={imageSrc}
|
|
|
|
|
crop={crop}
|
|
|
|
|
zoom={zoom}
|
|
|
|
|
rotation={rotation}
|
|
|
|
|
aspect={aspectRatio}
|
|
|
|
|
onCropChange={onCropChange}
|
|
|
|
|
onCropComplete={onCropCompleteHandler}
|
|
|
|
|
onZoomChange={setZoom}
|
|
|
|
|
cropShape={circularCrop ? 'round' : 'rect'}
|
|
|
|
|
showGrid={true}
|
|
|
|
|
/>
|
|
|
|
|
</Suspense>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Controls */}
|
aesthetic-improvements: fix spacing in components batch 2 (Action 11.5.1.6)
- Modals: CreatorModal (p-6 → p-8, space-y-6 → space-y-8)
- Seller: CreateProductView (space-y-6 → space-y-8, 2 instances)
- Social: ExploreView, GroupCard (space-y-6 → space-y-8, p-6 → p-8)
- Studio: ProjectsManager, CloudFileBrowser (space-y-6 → space-y-8, gap-6 → gap-8)
- Theme: ThemeSwitcher (space-y-6 → space-y-8, p-6 → p-8)
- Settings: AppearanceSettingsView (p-6 → p-8, space-y-6 → space-y-8)
- Library: PlaylistsView (space-y-6 → space-y-8)
- Upload: TagSuggestionsModal, LyricsEditorModal, BulkUploadModal (p-6 → p-8, space-y-6 → space-y-8)
- UI: modal, dialog, ImageCropper (p-6 → p-8)
- All spacing now aligned to 8px grid (32px standard padding)
- Action 11.5.1.6: Apply direction to all components - Batch 2 complete (spacing fixes)
2026-01-16 11:06:56 +00:00
|
|
|
<div className="p-8 bg-kodo-ink border-t border-kodo-steel space-y-4">
|
2026-01-07 18:39:21 +00:00
|
|
|
<div className="flex items-center gap-4">
|
2026-02-08 23:04:51 +00:00
|
|
|
<span className="text-xs text-muted-foreground w-16">Zoom</span>
|
|
|
|
|
<ZoomIn className="w-4 h-4 text-muted-foreground" />
|
2026-01-07 18:39:21 +00:00
|
|
|
<input
|
|
|
|
|
type="range"
|
|
|
|
|
value={zoom}
|
|
|
|
|
min={1}
|
|
|
|
|
max={3}
|
|
|
|
|
step={0.1}
|
|
|
|
|
aria-labelledby="Zoom"
|
|
|
|
|
onChange={(e) => setZoom(Number(e.target.value))}
|
|
|
|
|
className="flex-1 h-1 bg-kodo-steel rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:bg-kodo-cyan [&::-webkit-slider-thumb]:rounded-full"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-4">
|
2026-02-08 23:04:51 +00:00
|
|
|
<span className="text-xs text-muted-foreground w-16">Rotate</span>
|
|
|
|
|
<RotateCw className="w-4 h-4 text-muted-foreground" />
|
2026-01-07 18:39:21 +00:00
|
|
|
<input
|
|
|
|
|
type="range"
|
|
|
|
|
value={rotation}
|
|
|
|
|
min={0}
|
|
|
|
|
max={360}
|
|
|
|
|
step={1}
|
|
|
|
|
aria-labelledby="Rotation"
|
|
|
|
|
onChange={(e) => setRotation(Number(e.target.value))}
|
|
|
|
|
className="flex-1 h-1 bg-kodo-steel rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:bg-kodo-cyan [&::-webkit-slider-thumb]:rounded-full"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
aesthetic-improvements: align spacing to 8px grid (Action 11.2.1.3)
- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid
- Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px)
- Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation
- Modified files across all components to ensure consistent 8px grid alignment
- Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
2026-01-16 10:50:46 +00:00
|
|
|
<div className="flex justify-end gap-4 pt-2">
|
2026-01-13 18:47:57 +00:00
|
|
|
<Button variant="ghost" onClick={onCancel}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="primary"
|
|
|
|
|
onClick={handleSave}
|
|
|
|
|
icon={<Check className="w-4 h-4" />}
|
|
|
|
|
>
|
|
|
|
|
Apply Crop
|
|
|
|
|
</Button>
|
2026-01-07 18:39:21 +00:00
|
|
|
</div>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-01-13 18:47:57 +00:00
|
|
|
};
|