From 1b983b2276b5f1349ab97e958ee37385adb818e0 Mon Sep 17 00:00:00 2001 From: Andres Zenteno Date: Sun, 21 Dec 2025 22:25:17 -0500 Subject: [PATCH 01/14] feat: improve ModalImage --- .../Gallery/ModalImage/ModalImage.css | 88 ++++++++++++-- .../Gallery/ModalImage/ModalImage.tsx | 110 +++++++----------- 2 files changed, 123 insertions(+), 75 deletions(-) diff --git a/src/components/Gallery/ModalImage/ModalImage.css b/src/components/Gallery/ModalImage/ModalImage.css index 30e3849..dce81b2 100644 --- a/src/components/Gallery/ModalImage/ModalImage.css +++ b/src/components/Gallery/ModalImage/ModalImage.css @@ -1,12 +1,37 @@ +.modalImageOpen { + overflow: hidden; +} + +html.modalImageOpen, +body.modalImageOpen { + overflow: hidden; +} + +.modalCore.modalImageCore { + width: 96vw; + height: 96vh; + max-width: 96vw; + max-height: 96vh; +} + +.modalCore.modalImageCore .modalBody { + flex: 1 1 auto; + align-items: stretch; + justify-content: stretch; + max-height: 100%; + padding-top: 24px; +} + .photoModalWrap { align-items: stretch; display: flex; flex: 1 1; flex-direction: row; justify-content: center; - max-width: 90vw; /* Prevent the modal from exceeding the viewport width */ - max-height: 90vh; /* Prevent the modal from exceeding the viewport height */ + max-width: 96vw; /* Prevent the modal from exceeding the viewport width */ + max-height: 96vh; /* Prevent the modal from exceeding the viewport height */ box-sizing: border-box; /* Include padding and border in size calculations */ + height: 100%; } /* Styling for the image */ @@ -15,19 +40,56 @@ align-items: center; justify-content: center; background-color: var(--rich-black); - flex-grow: 1; /* Allow the imageBox to take up remaining space */ + flex: 1 1 auto; /* Allow the imageBox to take up remaining space */ + min-width: 0; max-width: 100%; /* Ensure the imageBox doesn't exceed the viewport width */ max-height: 100%; /* Ensure the imageBox doesn't exceed the viewport height */ overflow: hidden; /* Prevent image overflow */ box-sizing: border-box; /* Consistent sizing */ + position: relative; +} + +.imageBoxZoomed { + overflow: auto; } -.imageBox > img { +.imageStatus { + position: absolute; + inset: 0; + display: grid; + place-items: center; + color: var(--soft); + background: linear-gradient(180deg, var(--rich-black-transparent-75), var(--rich-black)); + text-align: center; + gap: 8px; + padding: 24px; +} + +.imageFallbackLink { + color: var(--color-white); + text-decoration: underline; +} + +.modalImage { object-fit: contain; /* Ensure the image retains aspect ratio */ display: block; max-width: 100%; /* Prevent the image from exceeding the container width */ max-height: 100%; /* Prevent the image from exceeding the container height */ margin: auto; /* Center the image within the container */ + user-select: none; + cursor: zoom-in; + opacity: 0; + transition: opacity 140ms ease, transform 140ms ease; +} + +.modalImageLoaded { + opacity: 1; +} + +.modalImageZoomed { + transform: scale(1.75); + transform-origin: center; + cursor: zoom-out; } /* Description box styling */ @@ -35,12 +97,13 @@ background-color: var(--rich-black-transparent-75); box-sizing: border-box; display: flex; - flex: 1 1 100%; + flex: 0 0 auto; flex-direction: column; - max-width: 500px; /* Prevent the infoBox from taking up too much space */ - min-width: 405px; /* Ensure the infoBox has a minimum width */ + width: min(420px, 100%); + min-width: 0; padding: 16px; /* Add padding for better readability */ word-break: break-word; /* Handle long text */ + overflow: auto; } .imgDescription { @@ -85,3 +148,14 @@ .multiLikeCount { display: flex; } + +@media (max-width: 900px) { + .photoModalWrap { + flex-direction: column; + } + + .infoBox { + width: 100%; + max-height: 40vh; + } +} diff --git a/src/components/Gallery/ModalImage/ModalImage.tsx b/src/components/Gallery/ModalImage/ModalImage.tsx index c676237..01696bc 100644 --- a/src/components/Gallery/ModalImage/ModalImage.tsx +++ b/src/components/Gallery/ModalImage/ModalImage.tsx @@ -12,85 +12,59 @@ export interface ComponentProps { } export const ModalImage: React.FC = ({ imgDescription, imgLikes, imgName, imgSrc, isOpen, onClose }) => { - const [imgStyle, setImgStyle] = React.useState({}); - - const elementRef = React.useRef(null); - // const [dimensions, setDimensions] = React.useState({ width: 0, height: 0 }); + const [isLoaded, setIsLoaded] = React.useState(false); + const [hasError, setHasError] = React.useState(false); + const [isZoomed, setIsZoomed] = React.useState(false); React.useEffect(() => { - const img = new Image(); - img.src = imgSrc; - - const calculateImageDimensions = () => { - // Image's natural dimensions - const { naturalWidth, naturalHeight } = img; - - // Calculate the aspect ratio of the image - const imageAspectRatio = naturalWidth / naturalHeight; - - // Viewport constraints - const maxWidth = window.innerWidth * 0.8; // 80vw - const maxHeight = window.innerHeight * 0.8; // 80vh - - // Final dimensions based on constraints - let finalWidth, finalHeight; - - if (imageAspectRatio > 1) { - // Landscape image: Width constrained by maxWidth - finalWidth = Math.min(maxWidth, naturalWidth); - finalHeight = finalWidth / imageAspectRatio; - - // If height exceeds maxHeight, adjust dimensions - if (finalHeight > maxHeight) { - finalHeight = maxHeight; - finalWidth = finalHeight * imageAspectRatio; - } - } else { - // Portrait image: Height constrained by maxHeight - finalHeight = Math.min(maxHeight, naturalHeight); - finalWidth = finalHeight * imageAspectRatio; - - // If width exceeds maxWidth, adjust dimensions - if (finalWidth > maxWidth) { - finalWidth = maxWidth; - finalHeight = finalWidth / imageAspectRatio; - } - } - - // Apply consistent styles - setImgStyle({ - width: `${finalWidth}px`, - height: `${finalHeight}px`, - }); - }; - - // Wait for the image to load and then calculate dimensions - img.onload = calculateImageDimensions; - - // Recalculate on window resize - window.addEventListener('resize', calculateImageDimensions); - - // Cleanup event listener on unmount - return () => { - window.removeEventListener('resize', calculateImageDimensions); - }; - }, [imgSrc]); // Recalculate only when the image source changes - - - + if (!isOpen) return; + setIsLoaded(false); + setHasError(false); + setIsZoomed(false); + }, [imgSrc, isOpen]); // const imgTitle =
{imgName && {imgName}}
const handleOnLike = React.useCallback((event: React.MouseEvent): void => { console.log('I like it'); }, []); + + const handleToggleZoom = React.useCallback(() => { + if (!isLoaded || hasError) return; + setIsZoomed(previous => !previous); + }, [hasError, isLoaded]); + return ( - +
-
- {imgName} +
+ {!isLoaded && !hasError &&
Loading…
} + {hasError && ( +
+
Couldn’t load image.
+ + Open original + +
+ )} + {imgName} setIsLoaded(true)} + onError={() => setHasError(true)} + onClick={handleToggleZoom} + />
- {imgDescription &&
{imgDescription}
} +
{imgDescription || null}
From 374326e41b6303de19d08e7e8b704501570ec29e Mon Sep 17 00:00:00 2001 From: Andres Zenteno Date: Sun, 21 Dec 2025 23:43:34 -0500 Subject: [PATCH 02/14] fix: clean code, security updates, imporove gallery --- package-lock.json | 21 ----- package.json | 1 - src/api/images.ts | 84 ++++++++++++++----- src/api/users.ts | 12 ++- src/components/App/App.tsx | 32 ++++--- src/components/App/AppInitializer.tsx | 15 +--- src/components/Common/AuthContext.tsx | 4 - src/components/Common/AuthProvider.tsx | 18 ---- src/components/Common/index.tsx | 3 - .../Gallery/ArchiveImage/ArchiveImage.css | 10 +++ .../Gallery/ArchiveImage/ArchiveImage.tsx | 14 ++-- .../Gallery/DeleteImage/DeleteImage.tsx | 3 +- .../Gallery/HideImage/HideImage.css | 10 +++ .../Gallery/HideImage/HideImage.tsx | 14 ++-- .../Gallery/ModalImage/ModalImage.css | 14 ++++ .../Gallery/ModalImage/ModalImage.tsx | 48 ++++++----- .../Gallery/ShowGallery/ShowGallery.tsx | 36 +++----- .../Gallery/UploadImage/UploadImage.tsx | 19 +++-- src/components/Layout/Header/Header.tsx | 74 ++++++++-------- src/components/Login/Login.tsx | 45 ++++------ src/components/index.tsx | 4 +- src/firebase.configuration.ts | 5 +- src/index.tsx | 9 +- src/state/actionCreators/index.tsx | 32 +++++-- src/state/actions/index.ts | 5 ++ src/state/reducers/imagesReducer.ts | 8 ++ src/state/reducers/userImagesReducer.ts | 8 ++ src/utils/logger.ts | 17 ++++ yarn.lock | 9 -- 29 files changed, 315 insertions(+), 259 deletions(-) delete mode 100644 src/components/Common/AuthContext.tsx delete mode 100644 src/components/Common/AuthProvider.tsx create mode 100644 src/utils/logger.ts diff --git a/package-lock.json b/package-lock.json index 1137caf..eac6621 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "@types/react-modal": "^3.16.3", - "@types/react-router-dom": "^5.1.5", "firebase": "^7.17.1", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -4730,26 +4729,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-router-dom": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", - "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", - "license": "MIT", - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router": "*" - } - }, - "node_modules/@types/react-router-dom/node_modules/@types/react": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz", - "integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==", - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, "node_modules/@types/react-router/node_modules/@types/react": { "version": "19.1.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz", diff --git a/package.json b/package.json index 868df84..3240d78 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "@types/react-modal": "^3.16.3", - "@types/react-router-dom": "^5.1.5", "firebase": "^7.17.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/src/api/images.ts b/src/api/images.ts index fefa4cd..8815489 100644 --- a/src/api/images.ts +++ b/src/api/images.ts @@ -1,38 +1,69 @@ import firebase from 'firebase/app'; -import { db, imagesDbCollection } from 'firebase.configuration'; +import { auth, db, imagesDbCollection } from 'firebase.configuration'; import { ImageInterface } from 'type'; import config from '../config'; +import { logger } from 'utils/logger'; // Firestore reference const imagesRef = db.collection(imagesDbCollection); +const getAuthToken = async (): Promise => { + const user = auth.currentUser; + + if (!user) { + throw new Error('User must be authenticated to perform this action.'); + } + + return user.getIdToken(); +}; + const mapSnapshotToImages = (snapshot: firebase.firestore.QuerySnapshot): ImageInterface[] => - snapshot.docs.map(doc => ({ - imgId: doc.id, - imgArchived: doc.data().imgArchived, - imgDescription: doc.data().imgDescription, - imgLikes: doc.data().imgLikes, - imgName: doc.data().imgName, - imgPrivate: doc.data().imgPrivate, - imgSrc: doc.data().imgSrc, - imgUploadDate: doc.data().imgUploadDate, - imgUserOwner: doc.data().imgUserOwner, - })); + snapshot.docs.map(doc => { + const data = doc.data(); + const uploadDateRaw = data.imgUploadDate as unknown; + const imgUploadDate = + typeof uploadDateRaw === 'number' + ? uploadDateRaw + : uploadDateRaw && typeof (uploadDateRaw as any).toMillis === 'function' + ? (uploadDateRaw as any).toMillis() + : 0; + + return { + imgId: doc.id, + imgArchived: Boolean(data.imgArchived), + imgDescription: data.imgDescription ?? '', + imgLikes: Number(data.imgLikes ?? 0), + imgName: data.imgName ?? '', + imgPrivate: Boolean(data.imgPrivate), + imgSrc: data.imgSrc ?? '', + imgUploadDate, + imgUserOwner: data.imgUserOwner ?? '', + }; + }); + +const sortNewestFirst = (images: ImageInterface[]) => + [...images].sort((a, b) => (b.imgUploadDate ?? 0) - (a.imgUploadDate ?? 0)); // Get only public, non-archived images ordered by upload date (newest first) export const getPublicImages = async (): Promise => { - const snapshot = await imagesRef.where('imgArchived', '==', false).get(); - return mapSnapshotToImages(snapshot) - .filter(img => img.imgPrivate === false) - .sort((a, b) => b.imgUploadDate - a.imgUploadDate); + const snapshot = await imagesRef + .where('imgArchived', '==', false) + .where('imgPrivate', '==', false) + .get(); + + return sortNewestFirst(mapSnapshotToImages(snapshot)); }; // Get images for a specific user; optionally include archived ones export const getUserImages = async (uid: string, includeArchived?: boolean): Promise => { - const snapshot = await imagesRef.where('imgUserOwner', '==', uid).get(); - return mapSnapshotToImages(snapshot) - .filter(img => includeArchived ? true : img.imgArchived === false) - .sort((a, b) => b.imgUploadDate - a.imgUploadDate); + let query: firebase.firestore.Query = imagesRef.where('imgUserOwner', '==', uid); + + if (!includeArchived) { + query = query.where('imgArchived', '==', false); + } + + const snapshot = await query.get(); + return sortNewestFirst(mapSnapshotToImages(snapshot)); }; // Function to set image privacy (private/public) @@ -51,16 +82,20 @@ export const archiveImage = async (image: ImageInterface, imgArchived: boolean) export const uploadImage = async (image: File): Promise => { const formData = new FormData(); formData.append('image', image); + const authToken = await getAuthToken(); try { // Make a POST request to the backend for image upload const response = await fetch(`${config.apiBaseUrl}/resize-upload`, { method: 'POST', + headers: { + Authorization: `Bearer ${authToken}`, + }, body: formData, }); const result = await response.json(); - console.log('Upload response:', result); + logger.debug('Upload response:', result); if (response.ok) { return result.url; // Return the uploaded image URL @@ -78,21 +113,24 @@ export const uploadImage = async (image: File): Promise => { export const deleteImage = async (image: ImageInterface) => { // Delete image document from Firestore const imageDocRef = db.collection(imagesDbCollection).doc(image.imgId); + const authToken = await getAuthToken(); + try { await imageDocRef.delete(); - console.log(`Firestore document deleted: ${image.imgId}`); + logger.debug('Firestore document deleted:', image.imgId); // Call the backend API to delete the image from Firebase Storage const response = await fetch(`${config.apiBaseUrl}/delete-image`, { method: 'POST', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${authToken}`, }, body: JSON.stringify({ imgName: image.imgName }), }); if (response.ok) { - console.log(`Image successfully deleted from Firebase Storage: ${image.imgName}`); + logger.debug('Image deleted from Firebase Storage:', image.imgName); } else { console.error('Error deleting image from Firebase Storage:', await response.text()); } diff --git a/src/api/users.ts b/src/api/users.ts index dec51f8..603aeb2 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -1,9 +1,13 @@ import { db, usersDbCollection } from 'firebase.configuration'; -// import { UserInterface } from 'type'; +import type { UserInterface } from 'type'; const usersRef = db.collection(usersDbCollection); -export const getUserData = async (userId: string | undefined) => { - const snapshot = await usersRef.doc(userId); - console.log(snapshot); +export const getUserData = async (userId: string | undefined): Promise => { + if (!userId) return null; + + const snapshot = await usersRef.doc(userId).get(); + if (!snapshot.exists) return null; + + return snapshot.data() as UserInterface; }; diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index cee48d1..8de3868 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -4,7 +4,7 @@ import { useSelector } from 'react-redux'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { RootState } from 'state/reducers'; import { AppInitializer } from './AppInitializer' -import { AuthProvider, Footer, Header, Login, ShowGallery, UploadImage } from 'components'; +import { Footer, Header, Login, ShowGallery, UploadImage } from 'components'; import './App.css'; export const App: React.FC = () => { @@ -14,22 +14,20 @@ export const App: React.FC = () => { return ( - - -
-
-
- - } /> - } /> - } /> - } /> - -
-
-
-
-
+ +
+
+
+ + } /> + } /> + } /> + } /> + +
+
+
+
); }; diff --git a/src/components/App/AppInitializer.tsx b/src/components/App/AppInitializer.tsx index 6c2586a..9b308e2 100644 --- a/src/components/App/AppInitializer.tsx +++ b/src/components/App/AppInitializer.tsx @@ -2,28 +2,19 @@ import React, { useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { auth } from 'firebase.configuration'; // Firebase auth instance import { actionCreators } from 'state'; // Redux actions for setting UID -import firebase from 'firebase/app'; // Import Firebase +import { logger } from 'utils/logger'; export const AppInitializer: React.FC = ({ children }) => { const dispatch = useDispatch(); useEffect(() => { - // Set Firebase auth persistence to LOCAL - auth.setPersistence(firebase.auth.Auth.Persistence.LOCAL) - .then(() => { - console.log('Firebase persistence set to LOCAL'); - }) - .catch((error) => { - console.error('Error setting Firebase persistence:', error); - }); - // Listen for Firebase auth state changes const unsubscribe = auth.onAuthStateChanged((user) => { if (user) { - console.log("Firebase user on refresh:", user.uid); // Log the user UID + logger.debug('Firebase auth state:', user.uid); dispatch(actionCreators.setUserUID(user.uid)); // Dispatch UID to Redux } else { - console.log("No user found after refresh"); + logger.debug('Firebase auth state: signed out'); dispatch(actionCreators.setUserUID(null)); // Set UID to null if no user } }); diff --git a/src/components/Common/AuthContext.tsx b/src/components/Common/AuthContext.tsx deleted file mode 100644 index 62881cf..0000000 --- a/src/components/Common/AuthContext.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react'; -import firebase from 'firebase/app'; - -export const AuthContext = React.createContext(null); diff --git a/src/components/Common/AuthProvider.tsx b/src/components/Common/AuthProvider.tsx deleted file mode 100644 index 70b185b..0000000 --- a/src/components/Common/AuthProvider.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import firebase from 'firebase/app'; -import { AuthContext } from './AuthContext'; -import { auth } from 'firebase.configuration'; - -export const AuthProvider: React.FC = ({ children }) => { - const [user, setUser] = React.useState(null); - - React.useEffect(() => { - const unsubscribe = auth.onAuthStateChanged(firebaseUser => { - setUser(firebaseUser); - }); - - return unsubscribe; - }, []); - - return {children}; -}; diff --git a/src/components/Common/index.tsx b/src/components/Common/index.tsx index 1d61d18..91ce5f9 100644 --- a/src/components/Common/index.tsx +++ b/src/components/Common/index.tsx @@ -1,4 +1 @@ -export { AuthContext } from './AuthContext'; -export { AuthProvider } from './AuthProvider'; - export { CoreModal } from './CoreModal'; diff --git a/src/components/Gallery/ArchiveImage/ArchiveImage.css b/src/components/Gallery/ArchiveImage/ArchiveImage.css index 284efdd..3249954 100644 --- a/src/components/Gallery/ArchiveImage/ArchiveImage.css +++ b/src/components/Gallery/ArchiveImage/ArchiveImage.css @@ -5,8 +5,18 @@ right: 0.5em; top: 0.5em; z-index: 10; + background: transparent; + border: 0; + padding: 0; + cursor: pointer; } .archiveIconWrapper i { display: inline-block; } + +.archiveIconWrapper:focus-visible { + outline: 2px solid var(--color-surface); + outline-offset: 2px; + border-radius: 6px; +} diff --git a/src/components/Gallery/ArchiveImage/ArchiveImage.tsx b/src/components/Gallery/ArchiveImage/ArchiveImage.tsx index 1d8e503..f284390 100644 --- a/src/components/Gallery/ArchiveImage/ArchiveImage.tsx +++ b/src/components/Gallery/ArchiveImage/ArchiveImage.tsx @@ -9,17 +9,21 @@ export interface ComponentProps { } export const ArchiveImage: React.FC = ({ imgArchived, imgData, handleArchiveImage }) => { + const label = imgArchived ? 'Unarchive image' : 'Archive image'; + return ( -
) => handleArchiveImage(imgData, imgArchived)} + aria-label={label} + onClick={() => handleArchiveImage(imgData, imgArchived)} > {!imgArchived ? ( - + ) : ( - + )} -
+ ); }; diff --git a/src/components/Gallery/DeleteImage/DeleteImage.tsx b/src/components/Gallery/DeleteImage/DeleteImage.tsx index a215e4b..f4efb67 100644 --- a/src/components/Gallery/DeleteImage/DeleteImage.tsx +++ b/src/components/Gallery/DeleteImage/DeleteImage.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { ImageInterface } from 'type'; import { deleteImage } from 'api'; +import { logger } from 'utils/logger'; import './DeleteImage.css'; export interface ComponentProps { @@ -11,7 +12,7 @@ export interface ComponentProps { export const handleDeleteImage = async (data: ImageInterface) => { try { await deleteImage(data); - console.log(`Image with ID ${data.imgId} deleted successfully`); + logger.debug('Image deleted successfully', data.imgId); } catch (error) { console.error('Error deleting image:', error); } diff --git a/src/components/Gallery/HideImage/HideImage.css b/src/components/Gallery/HideImage/HideImage.css index 5c98d39..3609465 100644 --- a/src/components/Gallery/HideImage/HideImage.css +++ b/src/components/Gallery/HideImage/HideImage.css @@ -5,8 +5,18 @@ right: 0.5em; top: 3em; z-index: 10; + background: transparent; + border: 0; + padding: 0; + cursor: pointer; } .hideIconWrapper i { display: inline-block; } + +.hideIconWrapper:focus-visible { + outline: 2px solid var(--color-surface); + outline-offset: 2px; + border-radius: 6px; +} diff --git a/src/components/Gallery/HideImage/HideImage.tsx b/src/components/Gallery/HideImage/HideImage.tsx index af64172..9a822d0 100644 --- a/src/components/Gallery/HideImage/HideImage.tsx +++ b/src/components/Gallery/HideImage/HideImage.tsx @@ -9,17 +9,21 @@ export interface ComponentProps { } export const HideImage: React.FC = ({ imgPrivate, imgData, handleHideImage }) => { + const label = imgPrivate ? 'Make image public' : 'Make image private'; + return ( -
) => handleHideImage(imgData, imgPrivate)} + aria-label={label} + onClick={() => handleHideImage(imgData, imgPrivate)} > {!imgPrivate ? ( - + ) : ( - + )} -
+ ); }; diff --git a/src/components/Gallery/ModalImage/ModalImage.css b/src/components/Gallery/ModalImage/ModalImage.css index dce81b2..6358292 100644 --- a/src/components/Gallery/ModalImage/ModalImage.css +++ b/src/components/Gallery/ModalImage/ModalImage.css @@ -149,6 +149,20 @@ body.modalImageOpen { display: flex; } +.likeButton { + background: transparent; + border: 0; + padding: 0; + display: inline-flex; + cursor: pointer; +} + +.likeButton:focus-visible { + outline: 2px solid var(--soft); + outline-offset: 2px; + border-radius: 6px; +} + @media (max-width: 900px) { .photoModalWrap { flex-direction: column; diff --git a/src/components/Gallery/ModalImage/ModalImage.tsx b/src/components/Gallery/ModalImage/ModalImage.tsx index 01696bc..4464067 100644 --- a/src/components/Gallery/ModalImage/ModalImage.tsx +++ b/src/components/Gallery/ModalImage/ModalImage.tsx @@ -24,9 +24,7 @@ export const ModalImage: React.FC = ({ imgDescription, imgLikes, }, [imgSrc, isOpen]); // const imgTitle =
{imgName && {imgName}}
- const handleOnLike = React.useCallback((event: React.MouseEvent): void => { - console.log('I like it'); - }, []); + const handleOnLike = React.useCallback((): void => {}, []); const handleToggleZoom = React.useCallback(() => { if (!isLoaded || hasError) return; @@ -61,25 +59,35 @@ export const ModalImage: React.FC = ({ imgDescription, imgLikes, onLoad={() => setIsLoaded(true)} onError={() => setHasError(true)} onClick={handleToggleZoom} - /> -
-
-
{imgDescription || null}
-
- - - + /> +
+
+
{imgDescription || null}
+
+ + +