diff --git a/frontend/src/components/Album/Album.tsx b/frontend/src/components/Album/Album.tsx index 45681613a..6e5a3aa69 100644 --- a/frontend/src/components/Album/Album.tsx +++ b/frontend/src/components/Album/Album.tsx @@ -49,7 +49,9 @@ const AlbumsView: React.FC = () => { return (
-

Albums

+

+ Albums +

-
No albums found.
+
+ No albums found. +
setIsCreateFormOpen(false)} @@ -114,7 +118,9 @@ const AlbumsView: React.FC = () => { ) : ( <>
-

Albums

+

+ Albums +

{showAdjustMenu && ( -
+
)} - + {onaspect && (
diff --git a/frontend/src/components/Navigation/Sidebar/AvatarCropper.tsx b/frontend/src/components/Navigation/Sidebar/AvatarCropper.tsx deleted file mode 100644 index c3776847b..000000000 --- a/frontend/src/components/Navigation/Sidebar/AvatarCropper.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React, { useState, useCallback } from 'react'; -import Cropper, { Area } from 'react-easy-crop'; - -interface AvatarCropperProps { - image: string; - onCropComplete: (croppedImage: string) => void; -} - -const AvatarCropper: React.FC = ({ - image, - onCropComplete, -}) => { - const [crop, setCrop] = useState({ x: 0, y: 0 }); - const [zoom, setZoom] = useState(1); - const [croppedAreaPixels, setCroppedAreaPixels] = useState(null); - const [croppedPreview, setCroppedPreview] = useState(null); - - // Add type annotations for parameters - const handleCropComplete = useCallback((_: Area, croppedAreaPixels: Area) => { - setCroppedAreaPixels(croppedAreaPixels); - }, []); - - const createImage = (url: string): Promise => { - return new Promise((resolve, reject) => { - const img = new Image(); - img.crossOrigin = 'anonymous'; - img.src = url; - img.onload = () => resolve(img); - img.onerror = (error) => reject(error); - }); - }; - - // Add type annotation for crop parameter - const getCroppedImg = async (imageSrc: string, crop: Area) => { - const image = await createImage(imageSrc); - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - - // Add null check for canvas context - if (!ctx) { - throw new Error('Canvas context not available'); - } - - canvas.width = crop.width; - canvas.height = crop.height; - - ctx.drawImage( - image, - crop.x, - crop.y, - crop.width, - crop.height, - 0, - 0, - crop.width, - crop.height, - ); - - return canvas.toDataURL('image/jpeg'); - }; - - const handleCropImage = async () => { - if (croppedAreaPixels) { - try { - const croppedImage = await getCroppedImg(image, croppedAreaPixels); - onCropComplete(croppedImage); - setCroppedPreview(croppedImage); - } catch (error) { - console.error('Cropping failed:', error); - } - } - }; - - return ( -
- {/* Cropper Container with Stylish Border */} -
-
- -
-
- - {/* Zoom Control with Labels */} -
-
- - Zoom - - - {zoom.toFixed(1)}x - -
-
- setZoom(Number(e.target.value))} - className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-gray-200 accent-blue-500 dark:bg-gray-600" - /> -
-
- - {/* Action Buttons */} -
- -
- - {/* Cropped Preview */} - {croppedPreview && ( -
-

- Preview -

-
- Cropped Preview -
-
- )} -
- ); -}; - -export default AvatarCropper; diff --git a/frontend/src/components/Navigation/Sidebar/CustomizationPopup.tsx b/frontend/src/components/Navigation/Sidebar/CustomizationPopup.tsx deleted file mode 100644 index 877aee072..000000000 --- a/frontend/src/components/Navigation/Sidebar/CustomizationPopup.tsx +++ /dev/null @@ -1,400 +0,0 @@ -import React, { useState } from 'react'; -import { X, Palette, Layout, Type, Image } from 'lucide-react'; -import { CustomStyles, presetThemes } from './styles'; - -// Define the props interface for the CustomizationPopup component -interface CustomizationPopupProps { - styles: CustomStyles; // Current styles - setStyles: React.Dispatch>; // Function to update styles - onClose: () => void; // Function to close the popup -} - -type TabType = 'presets' | 'colors' | 'layout' | 'typography' | 'background'; - -const CustomizationPopup: React.FC = ({ - styles, - setStyles, - onClose, -}) => { - const [activeTab, setActiveTab] = useState('presets'); - - /** - * Updates a specific style property in the `styles` state. - * @param key - The key of the style property to update. - * @param value - The new value for the style property. - */ - const updateStyle = (key: keyof CustomStyles, value: string | number) => { - setStyles((prev) => ({ ...prev, [key]: value })); - }; - - /** - * Handles file upload for background images or videos. - * @param event - The file input change event. - * @param type - The type of file being uploaded ('image' or 'video'). - */ - const handleFileUpload = ( - event: React.ChangeEvent, - type: 'image' | 'video', - ) => { - const file = event.target.files?.[0]; - if (file) { - const url = URL.createObjectURL(file); - if (type === 'image') { - updateStyle('backgroundImage', `url(${url})`); - updateStyle('backgroundVideo', ''); // Clear video if an image is set - } else { - updateStyle('backgroundVideo', url); - updateStyle('backgroundImage', ''); // Clear image if a video is set - } - } - }; - - /** - * Renders a control input for customizing a specific style property. - */ - const renderColorControl = (label: string, key: keyof CustomStyles) => ( -
-
- updateStyle(key, e.target.value)} - className="h-10 w-10 cursor-pointer rounded-md border-2 border-gray-200 shadow-sm dark:border-gray-600" - /> -
-
-
- -
- {styles[key]} -
-
-
- ); - - const renderRangeControl = ( - label: string, - key: keyof CustomStyles, - options?: { min?: number; max?: number; step?: number }, - ) => ( -
-
- - - {styles[key]} - -
- updateStyle(key, parseInt(e.target.value))} - className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700" - /> -
- {options?.min} - {options?.max} -
-
- ); - - const renderSelectControl = ( - label: string, - key: keyof CustomStyles, - options?: { choices?: string[] }, - ) => ( -
- - -
- ); - - const renderFileControl = ( - label: string, - key: keyof CustomStyles, - options?: { accept?: string }, - ) => ( -
- -
- - - {styles[key] && ( -
- - {String(styles[key]).substring(0, 25)}... - - -
- )} -
-
- ); - - const TabButton: React.FC<{ - active: boolean; - onClick: () => void; - icon: React.ReactNode; - label: string; - }> = ({ active, onClick, icon, label }) => ( - - ); - - const SectionHeader: React.FC<{ title: string }> = ({ title }) => ( -

- {title} -

- ); - - const renderPresetThemes = () => ( -
- -
- {Object.entries(presetThemes).map(([name, theme]) => ( - - ))} -
-
- ); - - const renderTabContent = () => { - switch (activeTab) { - case 'presets': - return renderPresetThemes(); - case 'colors': - return ( -
- - {renderColorControl('Background Color', 'bgColor')} - {renderColorControl('Text Color', 'textColor')} - {renderColorControl('Accent Color', 'activeBackgroundColor')} - {renderColorControl('Active Text Color', 'activeTextColor')} - {renderColorControl('Icon Color', 'iconColor')} - {renderColorControl('UI Background', 'uiBackgroundColor')} -
- ); - case 'layout': - return ( -
- - {renderRangeControl('Sidebar Width', 'sidebarWidth', { - min: 48, - max: 128, - step: 8, - })} - {renderRangeControl('Icon Size', 'iconSize', { - min: 16, - max: 32, - step: 2, - })} -
- ); - case 'typography': - return ( -
- - {renderRangeControl('Font Size', 'fontSize', { - min: 12, - max: 20, - step: 1, - })} - {renderSelectControl('Font Family', 'fontFamily', { - choices: [ - 'Inter, sans-serif', - "'SF Pro Display', sans-serif", - "'Roboto', sans-serif", - "'Open Sans', sans-serif", - ], - })} -
- ); - case 'background': - return ( -
- - {renderFileControl('Background Image', 'backgroundImage', { - accept: 'image/*', - })} - {renderFileControl('Background Video', 'backgroundVideo', { - accept: 'video/*', - })} -
- ); - default: - return
Select a tab
; - } - }; - - return ( -
- {styles.backgroundVideo && ( -
- ); -}; - -export default CustomizationPopup; diff --git a/frontend/src/components/Navigation/Sidebar/ImageCompressor.tsx b/frontend/src/components/Navigation/Sidebar/ImageCompressor.tsx deleted file mode 100644 index 29c1cb7f4..000000000 --- a/frontend/src/components/Navigation/Sidebar/ImageCompressor.tsx +++ /dev/null @@ -1,363 +0,0 @@ -import React, { useState, useCallback } from 'react'; -import { Upload, Download, Trash2, RefreshCw } from 'lucide-react'; - -// Define the CompressedImage interface -interface CompressedImage { - id: string; - originalFile: File; - originalSize: number; - compressedSize: number; - compressedBlob: Blob; - previewUrl: string; -} - -const ImageCompressor: React.FC = () => { - const [compressedImages, setCompressedImages] = useState( - [], - ); - const [isCompressing, setIsCompressing] = useState(false); - const [compressionLevel, setCompressionLevel] = useState(0.7); - const [maxWidth, setMaxWidth] = useState(1920); - const [maxHeight, setMaxHeight] = useState(1080); - - /** - * Compresses an image file. - * @param file - The image file to compress. - * @returns A promise that resolves to the compressed image object. - */ - const compressImage = useCallback( - async (file: File): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = (event) => { - const img = new Image(); - img.onload = () => { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - if (!ctx) { - reject('Failed to get canvas context'); - return; - } - - let width = img.width; - let height = img.height; - - // Adjust dimensions to fit within maxWidth and maxHeight - if (width > maxWidth) { - height *= maxWidth / width; - width = maxWidth; - } - if (height > maxHeight) { - width *= maxHeight / height; - height = maxHeight; - } - - canvas.width = width; - canvas.height = height; - - ctx.drawImage(img, 0, 0, width, height); - - // Convert canvas to Blob - canvas.toBlob( - (blob) => { - if (blob) { - const compressedImage: CompressedImage = { - id: Math.random().toString(36).substring(2, 9), - originalFile: file, - originalSize: file.size, - compressedSize: blob.size, - compressedBlob: blob, - previewUrl: URL.createObjectURL(blob), - }; - resolve(compressedImage); - } else { - reject('Failed to compress image'); - } - }, - 'image/jpeg', - compressionLevel, - ); - }; - img.onerror = () => reject('Image load failed'); - img.src = event.target?.result as string; - }; - reader.onerror = () => reject('File reading failed'); - reader.readAsDataURL(file); - }); - }, - [compressionLevel, maxWidth, maxHeight], - ); - - /** - * Handles file upload and compresses the selected images. - * @param event - The file input change event. - */ - const handleFileUpload = async ( - event: React.ChangeEvent, - ) => { - const files = event.target.files; - if (files && files.length > 0) { - setIsCompressing(true); - try { - const compressedFiles = await Promise.all( - Array.from(files).map((file) => - compressImage(file).catch((err) => { - console.error('Compression failed:', err); - return null; - }), - ), - ); - setCompressedImages((prev) => [ - ...prev, - ...compressedFiles.filter( - (file): file is CompressedImage => file !== null, - ), - ]); - } catch (error) { - console.error('Error compressing images:', error); - } finally { - setIsCompressing(false); - } - } - }; - - /** - * Handles downloading a compressed image. - * @param image - The compressed image to download. - */ - const handleDownload = (image: CompressedImage) => { - const url = URL.createObjectURL(image.compressedBlob); - const a = document.createElement('a'); - a.href = url; - a.download = `compressed_${image.originalFile.name}`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }; - - /** - * Handles removing a compressed image from the list. - * @param id - The ID of the compressed image to remove. - */ - const handleRemove = (id: string) => { - setCompressedImages((prev) => prev.filter((img) => img.id !== id)); - }; - - /** - * Handles recompressing an image. - * @param image - The compressed image to recompress. - */ - const handleRecompress = async (image: CompressedImage) => { - setIsCompressing(true); - try { - const recompressedImage = await compressImage(image.originalFile); - setCompressedImages((prev) => - prev.map((img) => (img.id === image.id ? recompressedImage : img)), - ); - } catch (error) { - console.error('Error recompressing image:', error); - } finally { - setIsCompressing(false); - } - }; - - return ( -
-

- Image Compressor -

- -
- {/* Compression Level Slider */} -
- - setCompressionLevel(parseFloat(e.target.value))} - className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-gray-200 accent-blue-500 dark:bg-gray-600" - /> -
- - {/* Max Width Slider */} -
- - setMaxWidth(parseInt(e.target.value))} - className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-gray-200 accent-blue-500 dark:bg-gray-600" - /> -
- - {/* Max Height Slider */} -
- - setMaxHeight(parseInt(e.target.value))} - className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-gray-200 accent-blue-500 dark:bg-gray-600" - /> -
-
- - {/* File Upload Button */} -
- - -
- - {/* Compressing Indicator */} - {isCompressing && ( -
-
-

- Compressing images... -

-
- )} - - {/* Compressed Images List */} -
    - {compressedImages.map((image) => ( -
  • -
    - - {image.originalFile.name} - -
    - {/* Download Button */} - - - {/* Recompress Button */} - - - {/* Remove Button */} - -
    -
    - - {/* Image Preview and Details */} -
    -
    - Preview -
    -
    -
    -

    - - Original: - - - {(image.originalSize / 1024).toFixed(2)} KB - -

    -

    - - Compressed: - - - {(image.compressedSize / 1024).toFixed(2)} KB - -

    -
    -
    -

    - Saved{' '} - {( - (image.originalSize - image.compressedSize) / - 1024 - ).toFixed(2)}{' '} - KB - - {( - ((image.originalSize - image.compressedSize) / - image.originalSize) * - 100 - ).toFixed(1)} - % - -

    -
    -
    -
    -
  • - ))} -
-
- ); -}; - -export default ImageCompressor; diff --git a/frontend/src/components/Navigation/Sidebar/Sidebar.tsx b/frontend/src/components/Navigation/Sidebar/Sidebar.tsx index ab93dbc99..9c4a498f1 100644 --- a/frontend/src/components/Navigation/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Navigation/Sidebar/Sidebar.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect } from 'react'; import { Link, useLocation } from 'react-router'; import { Home, @@ -6,15 +6,9 @@ import { Video, Images, Settings, - Palette, - FileArchiveIcon as FileCompress, BookImage, Lock, - User, } from 'lucide-react'; -import CustomizationPopup from './CustomizationPopup'; -import ImageCompressor from './ImageCompressor'; -import AvatarCropper from './AvatarCropper'; import { defaultStyles, type CustomStyles } from './styles'; import { ROUTES } from '@/constants/routes'; @@ -34,16 +28,7 @@ type CustomCSSProperties = React.CSSProperties & { const Sidebar: React.FC = () => { const location = useLocation(); - // const [] = useState(false); - const [showCustomize, setShowCustomize] = useState(false); - const [showImageCompressor, setShowImageCompressor] = - useState(false); - const [styles, setStyles] = useState(defaultStyles); - const [avatar, setAvatar] = useState(null); - const [isAvatarLoading, setIsAvatarLoading] = useState(false); - const [avatarError, setAvatarError] = useState(null); - const [showAvatarCropper, setShowAvatarCropper] = useState(false); - const [croppedAvatar, setCroppedAvatar] = useState(null); + const [styles] = useState(defaultStyles); // Check if the current path matches the given path const isActive = (path: string): boolean => location.pathname === path; @@ -53,54 +38,6 @@ const Sidebar: React.FC = () => { document.body.style.backgroundColor = styles.uiBackgroundColor; }, [styles.uiBackgroundColor]); - // Handle keyboard events for accessibility - const handleKeyDown = useCallback((event: React.KeyboardEvent): void => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - (event.target as HTMLElement).click(); - } - }, []); - - // Handle avatar file upload - const handleAvatarChange = async ( - event: React.ChangeEvent, - ): Promise => { - const file = event.target.files?.[0]; - setAvatarError(null); - - if (!file) return; - - if (!file.type.startsWith('image/')) { - setAvatarError('Please upload a valid image file (JPEG, PNG, GIF)'); - return; - } - - setIsAvatarLoading(true); - - try { - const reader = new FileReader(); - reader.onloadend = () => { - setAvatar(reader.result as string); - setIsAvatarLoading(false); - setShowAvatarCropper(true); - }; - reader.onerror = () => { - setAvatarError('Error reading file. Please try again.'); - setIsAvatarLoading(false); - }; - reader.readAsDataURL(file); - } catch (error) { - setAvatarError('An unexpected error occurred. Please try again.'); - setIsAvatarLoading(false); - } - }; - - // Handle avatar crop completion - const handleCropComplete = (croppedImage: string): void => { - setCroppedAvatar(croppedImage); - setShowAvatarCropper(false); - }; - // Define the sidebar style const sidebarStyle: CustomCSSProperties = { background: styles.bgColor, @@ -136,22 +73,6 @@ const Sidebar: React.FC = () => { return ( <> - {/* Background Video */} - {styles.backgroundVideo && ( -
- -
- )} - {/* Sidebar */}
- - {/* Customization Popup */} - - {showCustomize && ( -
-
- setShowCustomize(false)} - /> -
-
- )} - - {/* Image Compressor Popup */} - - {showImageCompressor && ( -
-
-
-

- Image Compressor -

- -
- -
-
- )} - - {/* Avatar Cropper Popup */} - {showAvatarCropper && ( -
-
-
-

- Crop Avatar -

-
- -
-
- )} ); };