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-02-12 01:09:29 +00:00
|
|
|
<div className="bg-muted flex items-center justify-center h-full text-foreground">
|
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 (
|
fix: UI remediation Phase 1 (S0-S5) + Phase 2 Sprint 6 shadow system
Phase 1:
- S0: Fix open redirect (safeNavigate), delete AuthContext/legacy auth, encrypt API keys, gitignore .env files
- S1: Split client.ts god object into 5 modules, unify toast system, delete unused Sidebar
- S2: Add glass button variant, migrate 32 z-index to SUMI tokens, fix card dark mode
- S3: Skip nav link, aria-hidden on icons, focus-visible ring fixes, alt attrs, aria-live regions
- S4: React.memo on list items, fix key={index}, loading=lazy on images
- S5: Branded loading screen, page transitions respect reduced-motion, LikeButton micro-interaction, i18n sidebar/header
Phase 2 Sprint 6:
- Wire Tailwind shadow utilities to SUMI tokens in @theme block (fixes 50+ files)
- Define shadow-card/shadow-card-hover tokens
- Remove dark:shadow-none workarounds from card.tsx (SUMI handles per-theme shadows)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 09:13:44 +00:00
|
|
|
<div className="fixed inset-0 z-[var(--sumi-z-modal)] flex items-center justify-center p-4 bg-background/95 backdrop-blur-sm">
|
fix: stabilize builds, tests, and lint across all stacks
Complete stabilization pass bringing all 3 stacks to green:
Frontend (apps/web/):
- Fix TypeScript nullability in useSeason.ts, useTimeOfDay.ts hooks
- Disable no-undef in ESLint config (TypeScript handles it; JSX misidentified)
- Rename 306 story imports from @storybook/react to @storybook/react-vite
- Fix conditional hook call in useMediaQuery.ts useIsTablet
- Move useQuery to top of LoginPage.tsx component
- Remove useless try/catch in GearFormModal.tsx
- Fix stale closure in ResetPasswordPage.tsx handleChange
- Make Storybook decorators (withRouter, withQueryClient, withToast, withAudio)
no-ops since global StorybookDecorator already provides these — prevents
nested Router / duplicate provider crashes in vitest-browser
- Fix nested MemoryRouter in 3 page stories (TrackDetail, PlaylistDetail, UserProfile)
- Update i18n initialization in test setup (await init before changeLanguage)
- Update ~30 test assertions from English to French to match i18n translations
- Update test assertions to match SUMI V3 design changes (shadow vs border)
- Fix remaining story type errors (PlayerError, PlaylistBatchActions,
TrackFilters, VirtualizedChatMessages)
Backend (veza-backend-api/):
- Fix response_test.go RespondWithAppError signature (2 args, not 3)
- Fix TestErrorContractAuthEndpoints expected error codes
(ErrCodeUnauthorized vs ErrCodeInvalidCredentials)
- Fix TestTrackHandler_GetTrackLikes_Success missing auth middleware setup
- Fix TestPlaybackAnalyticsService_GetTrackStats k-anonymity threshold
(needs 5 unique users, not 1)
- Replace NOW() PostgreSQL function with time.Now() parameter in marketplace
service for SQLite test compatibility
- Add missing AutoMigrate entries in marketplace_test.go
(ProductImage, ProductPreview, ProductLicense, ProductReview)
Results:
- Frontend TypeCheck: 617 errors -> 0 errors
- Frontend ESLint: 349 errors -> 0 errors
- Frontend Vitest: 196 failing tests -> 1 skipped (3396/3397 passing)
- Backend go vet: 1 error -> 0 errors
- Backend tests: 5 failing -> all 13 packages passing
- Rust: 150/150 tests passing (unchanged)
- Storybook audit: 0 errors across 1244 stories
Triage report: docs/TRIAGE_REPORT.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:48:07 +00:00
|
|
|
<div className="relative w-full max-w-2xl bg-muted rounded-xl shadow-2xl overflow-hidden flex flex-col h-layout-modal-sm">
|
2026-01-07 09:31:02 +00:00
|
|
|
{/* Header */}
|
fix: stabilize builds, tests, and lint across all stacks
Complete stabilization pass bringing all 3 stacks to green:
Frontend (apps/web/):
- Fix TypeScript nullability in useSeason.ts, useTimeOfDay.ts hooks
- Disable no-undef in ESLint config (TypeScript handles it; JSX misidentified)
- Rename 306 story imports from @storybook/react to @storybook/react-vite
- Fix conditional hook call in useMediaQuery.ts useIsTablet
- Move useQuery to top of LoginPage.tsx component
- Remove useless try/catch in GearFormModal.tsx
- Fix stale closure in ResetPasswordPage.tsx handleChange
- Make Storybook decorators (withRouter, withQueryClient, withToast, withAudio)
no-ops since global StorybookDecorator already provides these — prevents
nested Router / duplicate provider crashes in vitest-browser
- Fix nested MemoryRouter in 3 page stories (TrackDetail, PlaylistDetail, UserProfile)
- Update i18n initialization in test setup (await init before changeLanguage)
- Update ~30 test assertions from English to French to match i18n translations
- Update test assertions to match SUMI V3 design changes (shadow vs border)
- Fix remaining story type errors (PlayerError, PlaylistBatchActions,
TrackFilters, VirtualizedChatMessages)
Backend (veza-backend-api/):
- Fix response_test.go RespondWithAppError signature (2 args, not 3)
- Fix TestErrorContractAuthEndpoints expected error codes
(ErrCodeUnauthorized vs ErrCodeInvalidCredentials)
- Fix TestTrackHandler_GetTrackLikes_Success missing auth middleware setup
- Fix TestPlaybackAnalyticsService_GetTrackStats k-anonymity threshold
(needs 5 unique users, not 1)
- Replace NOW() PostgreSQL function with time.Now() parameter in marketplace
service for SQLite test compatibility
- Add missing AutoMigrate entries in marketplace_test.go
(ProductImage, ProductPreview, ProductLicense, ProductReview)
Results:
- Frontend TypeCheck: 617 errors -> 0 errors
- Frontend ESLint: 349 errors -> 0 errors
- Frontend Vitest: 196 failing tests -> 1 skipped (3396/3397 passing)
- Backend go vet: 1 error -> 0 errors
- Backend tests: 5 failing -> all 13 packages passing
- Rust: 150/150 tests passing (unchanged)
- Storybook audit: 0 errors across 1244 stories
Triage report: docs/TRIAGE_REPORT.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:48:07 +00:00
|
|
|
<div className="p-4 shadow-[0_1px_0_var(--border)] flex justify-between items-center bg-card">
|
2026-02-12 01:09:29 +00:00
|
|
|
<h3 className="font-bold text-foreground flex items-center gap-2">
|
2026-01-07 09:31:02 +00:00
|
|
|
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 */}
|
fix: stabilize builds, tests, and lint across all stacks
Complete stabilization pass bringing all 3 stacks to green:
Frontend (apps/web/):
- Fix TypeScript nullability in useSeason.ts, useTimeOfDay.ts hooks
- Disable no-undef in ESLint config (TypeScript handles it; JSX misidentified)
- Rename 306 story imports from @storybook/react to @storybook/react-vite
- Fix conditional hook call in useMediaQuery.ts useIsTablet
- Move useQuery to top of LoginPage.tsx component
- Remove useless try/catch in GearFormModal.tsx
- Fix stale closure in ResetPasswordPage.tsx handleChange
- Make Storybook decorators (withRouter, withQueryClient, withToast, withAudio)
no-ops since global StorybookDecorator already provides these — prevents
nested Router / duplicate provider crashes in vitest-browser
- Fix nested MemoryRouter in 3 page stories (TrackDetail, PlaylistDetail, UserProfile)
- Update i18n initialization in test setup (await init before changeLanguage)
- Update ~30 test assertions from English to French to match i18n translations
- Update test assertions to match SUMI V3 design changes (shadow vs border)
- Fix remaining story type errors (PlayerError, PlaylistBatchActions,
TrackFilters, VirtualizedChatMessages)
Backend (veza-backend-api/):
- Fix response_test.go RespondWithAppError signature (2 args, not 3)
- Fix TestErrorContractAuthEndpoints expected error codes
(ErrCodeUnauthorized vs ErrCodeInvalidCredentials)
- Fix TestTrackHandler_GetTrackLikes_Success missing auth middleware setup
- Fix TestPlaybackAnalyticsService_GetTrackStats k-anonymity threshold
(needs 5 unique users, not 1)
- Replace NOW() PostgreSQL function with time.Now() parameter in marketplace
service for SQLite test compatibility
- Add missing AutoMigrate entries in marketplace_test.go
(ProductImage, ProductPreview, ProductLicense, ProductReview)
Results:
- Frontend TypeCheck: 617 errors -> 0 errors
- Frontend ESLint: 349 errors -> 0 errors
- Frontend Vitest: 196 failing tests -> 1 skipped (3396/3397 passing)
- Backend go vet: 1 error -> 0 errors
- Backend tests: 5 failing -> all 13 packages passing
- Rust: 150/150 tests passing (unchanged)
- Storybook audit: 0 errors across 1244 stories
Triage report: docs/TRIAGE_REPORT.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:48:07 +00:00
|
|
|
<div className="p-8 bg-card shadow-[0_-1px_0_var(--border)] 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))}
|
ui(tokens): migrate kodo-cyan to primary (51 files, 88 instances)
Replace legacy text-kodo-cyan/border-kodo-cyan/bg-kodo-cyan with semantic
text-primary/border-primary/bg-primary across 51 components.
The brand primary color now uses the design system token, enabling proper
theme adaptation. Covers UI primitives, search, dashboard, chat, playlists,
settings, social, marketplace, and auth components.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 23:19:12 +00:00
|
|
|
className="flex-1 h-1 bg-muted 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-primary [&::-webkit-slider-thumb]:rounded-full"
|
2026-01-07 18:39:21 +00:00
|
|
|
/>
|
|
|
|
|
</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))}
|
ui(tokens): migrate kodo-cyan to primary (51 files, 88 instances)
Replace legacy text-kodo-cyan/border-kodo-cyan/bg-kodo-cyan with semantic
text-primary/border-primary/bg-primary across 51 components.
The brand primary color now uses the design system token, enabling proper
theme adaptation. Covers UI primitives, search, dashboard, chat, playlists,
settings, social, marketplace, and auth components.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 23:19:12 +00:00
|
|
|
className="flex-1 h-1 bg-muted 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-primary [&::-webkit-slider-thumb]:rounded-full"
|
2026-01-07 18:39:21 +00:00
|
|
|
/>
|
|
|
|
|
</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
|
|
|
};
|