diff --git a/src/main.tsx b/src/main.tsx index 78f351eb..ac7175fe 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -8,6 +8,7 @@ import { Provider } from 'react-redux'; import { store } from './store/store'; import { ModalProvider } from './ModalContext.jsx'; import { ChangePasswordProvider } from './components/Context/ChangePasswordContext.jsx'; +import { ModalProvider1 } from './pages/Gallary/ModalContext.jsx'; createRoot(document.getElementById('root')!).render( @@ -16,7 +17,9 @@ createRoot(document.getElementById('root')!).render( + + diff --git a/src/pages/Gallary/ImageGallary.jsx b/src/pages/Gallary/ImageGallary.jsx index c0939fcd..5b3fca0d 100644 --- a/src/pages/Gallary/ImageGallary.jsx +++ b/src/pages/Gallary/ImageGallary.jsx @@ -1,8 +1,341 @@ -import React from "react"; -import { ComingSoonPage } from "../Misc/ComingSoonPage"; +import React, { useState, useEffect, useRef } from "react"; +import "./ImageGallery.css"; +import { ImageGalleryAPI } from "./ImageGalleryAPI"; // Assuming this exists +import moment from "moment"; +import { useSelector } from "react-redux"; // Assuming Redux is set up +import { useModal } from "./ModalContext"; // Assuming ModalContext exists +import ImagePop from "./ImagePop"; // Assuming ImagePop component exists +import Avatar from "../../components/common/Avatar"; // Assuming Avatar component exists -const ImageGallary = () => { - return ; +const ImageGallery = () => { + const [images, setImages] = useState([]); + const selectedProjectId = useSelector((store) => store.localVariables.projectId); + const { openModal } = useModal(); + + const [selectedFilters, setSelectedFilters] = useState({ + building: [], + floor: [], + activity: [], + uploadedBy: [], + workCategory: [], + workArea: [], + }); + + const [isFilterPanelOpen, setIsFilterPanelOpen] = useState(false); // Controls filter drawer + const [hoveredImage, setHoveredImage] = useState(null); // For image hover description + const [loading, setLoading] = useState(true); + + const imageGroupRefs = useRef({}); // To reference horizontal scroll containers + const filterPanelRef = useRef(null); // Ref for the filter panel for click-outside + const filterButtonRef = useRef(null); // Ref for the filter button for click-outside + + // Click outside handler for the filter panel + useEffect(() => { + const handleClickOutside = (event) => { + // Close filter panel if click is outside the panel and not on the toggle button itself + if ( + filterPanelRef.current && + !filterPanelRef.current.contains(event.target) && + filterButtonRef.current && + !filterButtonRef.current.contains(event.target) + ) { + setIsFilterPanelOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + // Fetch images when the selectedProjectId changes + useEffect(() => { + if (!selectedProjectId) return; + + setLoading(true); + ImageGalleryAPI.ImagesGet(selectedProjectId) + .then((res) => { + setImages(res.data); + }) + .catch((err) => { + console.error("Error fetching images:", err); + }) + .finally(() => { + setLoading(false); + }); + }, [selectedProjectId]); + + // Helper functions to get unique filter values + const getUniqueValues = (key) => [ + ...new Set(images.map((img) => img[key]).filter(Boolean)), + ]; + + const getUniqueUploadedByUsers = () => [ + ...new Set( + images + .map((img) => { + const firstName = img.uploadedBy?.firstName || ""; + const lastName = img.uploadedBy?.lastName || ""; + return `${firstName} ${lastName}`.trim(); + }) + .filter(Boolean) + ), + ]; + + const getUniqueWorkCategories = () => [ + ...new Set(images.map((img) => img.workCategoryName).filter(Boolean)), + ]; + + // Derive filter options + const buildings = getUniqueValues("buildingName"); + const floors = getUniqueValues("floorName"); + const activities = getUniqueValues("activityName"); + const workAreas = getUniqueValues("workAreaName"); + const uploadedByUsers = getUniqueUploadedByUsers(); + const workCategories = getUniqueWorkCategories(); + + // Toggle selected filters + const toggleFilter = (type, value) => { + setSelectedFilters((prev) => { + const current = prev[type]; + return { + ...prev, + [type]: current.includes(value) + ? current.filter((v) => v !== value) + : [...current, value], + }; + }); + }; + + // Filter images based on selected filters + const filteredImages = images.filter( + (img) => + (selectedFilters.building.length === 0 || + selectedFilters.building.includes(img.buildingName)) && + (selectedFilters.floor.length === 0 || + selectedFilters.floor.includes(img.floorName)) && + (selectedFilters.activity.length === 0 || + selectedFilters.activity.includes(img.activityName)) && + (selectedFilters.workArea.length === 0 || + selectedFilters.workArea.includes(img.workAreaName)) && + (selectedFilters.uploadedBy.length === 0 || + selectedFilters.uploadedBy.includes( + `${img.uploadedBy?.firstName || ""} ${ + img.uploadedBy?.lastName || "" + }`.trim() + )) && + (selectedFilters.workCategory.length === 0 || + selectedFilters.workCategory.includes(img.workCategoryName)) + ); + + // Group images by Activity, Uploader, and Work Area + const imagesByActivityUser = {}; + filteredImages.forEach((img) => { + const userName = `${img.uploadedBy?.firstName || ""} ${ + img.uploadedBy?.lastName || "" + }`.trim(); + const workArea = img.workAreaName || "Unknown"; // Handle cases where workAreaName might be null/undefined + const key = `${img.activityName}__${userName}__${workArea}`; + if (!imagesByActivityUser[key]) imagesByActivityUser[key] = []; + imagesByActivityUser[key].push(img); + }); + + // Scroll functionality for horizontal image groups + const scrollLeft = (key) => { + imageGroupRefs.current[key]?.scrollBy({ left: -200, behavior: "smooth" }); + }; + + const scrollRight = (key) => { + imageGroupRefs.current[key]?.scrollBy({ left: 200, behavior: "smooth" }); + }; + + // Helper function to render filter categories dropdowns + const renderFilterCategory = (label, items, type) => ( +
+
+
+ {label} + {selectedFilters[type].length > 0 && ( + + )} +
+
+ {items.map((item) => ( + + ))} +
+
+ ); + + return ( +
+
+
+ {loading ? ( +
+
+
+ ) : Object.entries(imagesByActivityUser).length > 0 ? ( + // Render each grouped section of images + Object.entries(imagesByActivityUser).map(([key, imgs]) => { + // Destructure the key to get activity, user, and work area + const [activity, userName, workArea] = key.split("__"); + // Get details from the first image in the group (assuming common details for the group) + const { buildingName, floorName, uploadedAt, workCategoryName } = + imgs[0]; + const date = moment(uploadedAt).format("YYYY-MM-DD"); + const time = moment(uploadedAt).format("hh:mm A"); + + return ( +
+
+
+
+ +
+ + {imgs[0].uploadedBy?.firstName}{" "} + {imgs[0].uploadedBy?.lastName} + + + {date} {time} + +
+
+
+ + {/* Location and Work Category display */} +
+
+ {buildingName} > {floorName} > {workArea} >{" "} + {activity} +
+ {workCategoryName && ( +
+ {workCategoryName} +
+ )} +
+
+ +
+ {/* Left scroll arrow */} + +
(imageGroupRefs.current[key] = el)} + > + {/* Render individual image cards within the group */} + {imgs.map((img, idx) => { + const hoverDate = moment(img.uploadedAt).format( + "YYYY-MM-DD" + ); + const hoverTime = moment(img.uploadedAt).format( + "hh:mm A" + ); + + return ( +
openModal()} + onMouseEnter={() => setHoveredImage(img)} + onMouseLeave={() => setHoveredImage(null)} + > +
+ {`Image +
+ {/* Hover description for image details */} + {hoveredImage === img && ( +
+

+ Date: {hoverDate} +

+

+ Time: {hoverTime} +

+

+ Activity:{" "} + {img.activityName} +

+
+ )} +
+ ); + })} +
+ {/* Right scroll arrow */} + +
+
+ ); + }) + ) : ( +

+ No images match the selected filters. +

+ )} +
+
+ + {/* Filter drawer section */} +
+ +
+ {renderFilterCategory("Building", buildings, "building")} + {renderFilterCategory("Floor", floors, "floor")} + {renderFilterCategory("Work Area", workAreas, "workArea")} + {renderFilterCategory("Activity", activities, "activity")} + {renderFilterCategory("Uploaded By (User)", uploadedByUsers, "uploadedBy")} + {renderFilterCategory("Work Category", workCategories, "workCategory")} +
+
+
+ ); }; -export default ImageGallary; +export default ImageGallery; + diff --git a/src/pages/Gallary/ImageGallery.css b/src/pages/Gallary/ImageGallery.css new file mode 100644 index 00000000..1795d188 --- /dev/null +++ b/src/pages/Gallary/ImageGallery.css @@ -0,0 +1,429 @@ +.gallery-container { + display: grid; /* Use CSS Grid for layout */ + /* + * MODIFIED: When filter is closed, main content takes 1fr, + * and the filter column is just big enough for the small floating button (e.g., 50px). + */ + grid-template-columns: 1fr 50px; + gap: 0px; /* Gap between main content and filter */ + padding: 25px; + font-family: sans-serif; + height: calc(100vh - 20px); + box-sizing: border-box; + background-color: #f7f9fc; + transition: grid-template-columns 0.3s ease-in-out; /* Smooth transition for column resizing */ +} + +.gallery-container.filter-panel-open { + /* When open, main content shrinks, filter expands to 250px */ + grid-template-columns: 1fr 250px; +} + +/* Main content area (images) - No changes needed here */ +.main-content { + overflow-y: auto; + max-height: 100%; + box-sizing: border-box; + scrollbar-width: thin; + scrollbar-color: #a7a7a7 #f1f1f1; +} + +.main-content::-webkit-scrollbar { + width: 8px; +} + +.main-content::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 10px; +} + +.main-content::-webkit-scrollbar-thumb { + background: #a7a7a7; + border-radius: 10px; +} + +.main-content::-webkit-scrollbar-thumb:hover { + background: #888; +} + +/* New: Wrapper for the filter drawer and its button */ +.filter-drawer-wrapper { + flex-shrink: 0; + max-height: 100%; + box-sizing: border-box; + position: relative; /* Essential for positioning the filter panel within */ + /* Hides the scrollbar from the wrapper itself, as the panel will handle its own scrolling. */ + scrollbar-width: none; /* For Firefox */ + -ms-overflow-style: none; /* For IE and Edge */ +} + +.filter-drawer-wrapper::-webkit-scrollbar { + display: none; /* For Chrome, Safari, and Opera */ +} + +.filter-button { + background-color: #6366f1; + color: white; + padding: 8px 12px; + font-size: 14px; + border: none; + border-radius: 6px; + cursor: pointer; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + /* Added padding to transition properties for smoothness */ + transition: background-color 0.2s ease, box-shadow 0.2s ease, width 0.3s ease-in-out, height 0.3s ease-in-out, border-radius 0.3s ease-in-out, padding 0.3s ease-in-out; + + /* Floating / Positioning */ + position: absolute; + top: 0; + right: 0; + height: 40px; /* Fixed height when closed */ + width: 40px; /* Fixed width when closed (making it square) */ + z-index: 100; /* Ensure it stays on top */ +} + +/* When the filter panel is open, the button should match its width and blend with the panel */ +.gallery-container.filter-panel-open .filter-button { + width: calc(100% - 16px); /* Match filter-panel width minus its padding (8px on each side) */ + height: auto; /* Allow height to adjust for text content */ + padding: 8px 12px; /* Restore padding when expanded */ + border-radius: 6px 6px 0 0; /* Adjust border-radius to blend with panel below */ + justify-content: space-between; /* Space between "Filter" text and "X" icon */ +} + +/* Add a class for the button when the panel is closed to show only icon */ +.filter-button.closed-icon { + padding: 0; /* Remove padding to make it compact */ + font-size: 20px; /* Make icon larger when it's just an icon */ +} + +.filter-button:hover { + background-color: #4f46e5; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +/* The actual panel that contains all filter categories */ +.filter-panel { + display: flex; /* Always display as flex to manage children */ + background-color: #fff; + border: 1px solid #e5e7eb; + border-radius: 6px; + padding: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + width: 100%; + box-sizing: border-box; + flex-direction: column; + gap: 12px; + max-height: 0; + /* Use overflow-y: hidden for the max-height transition to work smoothly */ + overflow-y: hidden; + opacity: 0; + transform: translateY(-10px); + /* Added border-radius to transition properties for smoothness */ + transition: max-height 0.3s ease-out, opacity 0.3s ease-out, transform 0.3s ease-out, border-radius 0.3s ease-in-out; + + /* Position it below the button when open */ + margin-top: 40px; /* Account for the button's fixed height */ +} + +.filter-panel.open { + max-height: 1000px; /* A value larger than the expected height of content */ + opacity: 1; + transform: translateY(0); + /* Remove top radius to blend with button when open */ + border-top-left-radius: 0; + border-top-right-radius: 0; +} + + +/* ... (rest of your CSS remains the same) ... */ + +/* Individual dropdown sections within the filter panel */ +.dropdown-content { + display: block !important; + position: static; + background-color: #f9fafb; + box-shadow: none; + padding: 4px 0; + margin-top: 0; + border-radius: 0 0 4px 4px; + max-height: unset; + overflow-y: visible; +} + +.dropdown-content label { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 10px; + font-size: 12px; + font-weight: 500; + color: #333; + cursor: pointer; + transition: background 0.2s; +} + +.dropdown-content label:hover { + background-color: #eef2ff; +} + +.dropdown-content input[type="checkbox"] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 16px; + height: 16px; + border-radius: 3px; + border: 1px solid #c7d2fe; + background-color: #fff; + cursor: pointer; + position: relative; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease-in-out; +} + +.dropdown-content input[type="checkbox"]:checked { + background-color: #6366f1; + border-color: #6366f1; +} + +.dropdown-content input[type="checkbox"]:checked::after { + content: '✔'; + font-size: 10px; + color: white; + position: absolute; +} + + +.dropdown-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 10px 5px; + border-bottom: 1px solid #eee; + margin-bottom: 6px; +} + +.clear-button { + font-size: 10px; + background: none; + border: none; + color: #6366f1; + cursor: pointer; + padding: 3px 6px; + border-radius: 4px; + transition: background-color 0.2s ease; +} + +.clear-button:hover { + background-color: #eef2ff; +} + +/* --- Image Card Section --- */ +.grouped-section { + margin-bottom: 5px; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 10px; + background-color: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.group-heading { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 3px; + font-size: 12px; + flex-wrap: wrap; + padding-bottom: 8px; + border-bottom: 1px dashed #eee; +} + +.group-heading > div { + margin-right: 15px; +} + +.datetime-line { + font-size: 12px; + color: #777; + margin-top: 0px; + +} + +.location-line { + font-weight: 600; + font-size: 12px; + color: #555; + text-align: right; /* Keep this if you want the whole block aligned right */ + display: flex; /* Use flexbox to manage children's layout */ + flex-direction: column; /* Stack children vertically */ + align-items: flex-end; /* Align items to the end (right) if text-align: right is desired */ +} +.work-category-display { + /* Basic styling for the work category, if needed */ + margin-top: 4px; /* Add some space above it */ + padding: 2px 6px; + /* A light background for better visibility */ + border-radius: 5px; + font-size: 12px; + /* Override the parent's bold if desired */ + color: #555; +} + +/* New: Wrapper for image group and arrows */ +.image-group-wrapper { + position: relative; + display: flex; + align-items: center; + padding: 0 0px; +} + +.image-group-horizontal { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + overflow-x: hidden; + gap: 3px; + padding-bottom: 8px; + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; + width: 100%; +} +.scroll-arrow { + background-color: rgba(0, 0, 0, 0.5); + color: white; + border: none; + border-radius: 50%; /* This makes it a circle */ + width: 30px; /* Ensure width and height are equal */ + height: 44px; /* Ensure width and height are equal */ + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + font-size: 18px; + z-index: 10; + position: absolute; + top: 50%; + transform: translateY(-50%); + opacity: 0; + transition: opacity 0.3s ease, background-color 0.2s ease; + pointer-events: none; +} + +.image-group-wrapper:hover .scroll-arrow { + opacity: 1; + pointer-events: auto; +} + +.scroll-arrow:hover { + background-color: rgba(0, 0, 0, 0.7); +} + +.left-arrow { + left: 5px; +} + +.right-arrow { + right: 5px; +} + +.image-card { + width: 150px; + border: 1px solid #ddd; + border-radius: 8px; + background: #fff; + cursor: pointer; + overflow: hidden; + flex-shrink: 0; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); + transition: transform 0.2s ease, box-shadow 0.2s ease; + position: relative; +} + +hr { + margin: 0rem 0; + color: var(--bs-border-color); + border: 0; + border-top: var(--bs-border-width) solid; + opacity: 1; +} + +.image-card:hover { + transform: translateY(-2px) scale(1.03); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); +} + +.image-wrapper img { + width: 100%; + height: 100px; + object-fit: cover; + display: block; + border-radius: 8px 8px 0 0; +} + +/* NEW: Styles for the hover description */ +.image-hover-description { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + background-color: rgba(0, 0, 0, 0.75); + color: white; + padding: 5px 8px; + box-sizing: border-box; + font-size: 11px; + line-height: 1.4; + text-align: left; + opacity: 0; + transform: translateY(100%); + transition: opacity 0.2s ease-out, transform 0.2s ease-out; + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; + pointer-events: none; +} + +.image-card:hover .image-hover-description { + opacity: 1; + transform: translateY(0); +} + +.image-hover-description p { + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.spinner-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 200px; +} + +.spinner { + border: 6px solid #f3f3f3; + border-top: 6px solid #6658f6; + border-radius: 50%; + width: 40px; + height: 40px; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + + diff --git a/src/pages/Gallary/ImageGalleryAPI.jsx b/src/pages/Gallary/ImageGalleryAPI.jsx new file mode 100644 index 00000000..0c844b35 --- /dev/null +++ b/src/pages/Gallary/ImageGalleryAPI.jsx @@ -0,0 +1,7 @@ +import { api } from "../../utils/axiosClient"; + +export const ImageGalleryAPI = { + + ImagesGet: (projectId) => + api.get(`/api/image/images/${projectId}`), +} \ No newline at end of file diff --git a/src/pages/Gallary/ImagePop.css b/src/pages/Gallary/ImagePop.css new file mode 100644 index 00000000..dfdb549c --- /dev/null +++ b/src/pages/Gallary/ImagePop.css @@ -0,0 +1,103 @@ +.image-modal-overlay { + position: fixed; + top: 0; + left: 0; + z-index: 9999; /* High z-index to ensure it's on top */ + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.85); /* Dark semi-transparent background */ + display: flex; + justify-content: center; + align-items: center; +} + +.image-modal-content { + background: #fff; + padding: 24px; + max-width: 90%; /* Responsive max-width */ + max-height: 100%; /* Responsive max-height */ + border-radius: 12px; + position: relative; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); + text-align: center; + display: flex; /* Use flexbox for internal layout */ + flex-direction: column; + align-items: center; + justify-content: center; +} + +.modal-image { + max-width: 100%; + max-height: 70vh; /* Limits image height to 70% of viewport height */ + border-radius: 10px; + object-fit: contain; /* Ensures the whole image is visible without cropping */ + margin-bottom: 20px; + flex-shrink: 0; /* Prevent image from shrinking if content is too large */ +} + +.image-details { + text-align: left; + color: #444; + font-size: 14px; + line-height: 1.4; + margin: 0; + padding: 0; + width: 100%; /* Ensure details section takes full width */ +} + +.image-details p { + margin: 4px 0; /* Reduce vertical space between lines in details */ +} + +.close-button { + position: absolute; + top: 1px; /* Position relative to the modal content */ + right: 8px; + font-size: 30px; + background: none; + border: none; + color: black; /* White color for visibility on dark overlay */ + cursor: pointer; + padding: 0; + line-height: 1; + z-index: 10000; /* Ensure it's above everything else */ +} + +/* Styles for the navigation buttons */ +.nav-button { + position: absolute; + top: 50%; /* Vertically center them */ + transform: translateY(-50%); /* Adjust for perfect vertical centering */ + background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent background */ + color: white; + border: none; + padding: 10px 15px; + font-size: 30px; + cursor: pointer; + z-index: 1000; /* Ensure buttons are above the image */ + border-radius: 50%; /* Make them circular */ + width: 50px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.3s ease; /* Smooth hover effect */ +} + +.nav-button:hover { + background-color: rgba(0, 0, 0, 0.8); /* Darker on hover */ +} + +.nav-button.prev-button { + left: 0px; /* Position left arrow */ +} + +.nav-button.next-button { + right: 0px; /* Position right arrow */ +} + +/* Style for disabled buttons (optional) */ +.nav-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/src/pages/Gallary/ImagePop.jsx b/src/pages/Gallary/ImagePop.jsx new file mode 100644 index 00000000..62f4b8da --- /dev/null +++ b/src/pages/Gallary/ImagePop.jsx @@ -0,0 +1,94 @@ +import React, { useState, useEffect } from "react"; +import "./ImagePop.css"; +import { useModal } from "./ModalContext"; +import moment from "moment"; + +const ImagePop = ({ images, initialIndex = 0 }) => { + const { closeModal } = useModal(); + // State to keep track of the currently displayed image's index + const [currentIndex, setCurrentIndex] = useState(initialIndex); + + // Effect to update currentIndex if the initialIndex prop changes (e.g., if the modal is reused) + useEffect(() => { + setCurrentIndex(initialIndex); + }, [initialIndex, images]); + + // If no images are provided or the array is empty, don't render + if (!images || images.length === 0) return null; + + // Get the current image based on currentIndex + const image = images[currentIndex]; + + // Fallback if for some reason the image at the current index doesn't exist + if (!image) return null; + + // Format details for display + const fullName = `${image.uploadedBy?.firstName || ""} ${ + image.uploadedBy?.lastName || "" + }`.trim(); + const date = moment(image.uploadedAt).format("YYYY-MM-DD"); + const time = moment(image.uploadedAt).format("hh:mm A"); + + // Handler for navigating to the previous image + const handlePrev = () => { + setCurrentIndex((prevIndex) => Math.max(0, prevIndex - 1)); + }; + + // Handler for navigating to the next image + const handleNext = () => { + setCurrentIndex((prevIndex) => + Math.min(images.length - 1, prevIndex + 1) + ); + }; + + // Determine if previous/next buttons should be enabled/visible + const hasPrev = currentIndex > 0; + const hasNext = currentIndex < images.length - 1; + + return ( +
+
+ {/* Close button */} + + + {/* Previous button, only shown if there's a previous image */} + {hasPrev && ( + + )} + + {/* The main image display */} + Preview + + {/* Next button, only shown if there's a next image */} + {hasNext && ( + + )} + + {/* Image details */} +
+

+ 👤 Uploaded By: {fullName} +

+

+ 📅 Date: {date} {time} +

+

+ 🏢 Location: {image.buildingName} >{" "} + {image.floorName} > {image.activityName} +

+

+ 📝 Comments: {image.comment} +

+
+
+
+ ); +}; + +export default ImagePop; \ No newline at end of file diff --git a/src/pages/Gallary/ModalContext.jsx b/src/pages/Gallary/ModalContext.jsx new file mode 100644 index 00000000..a84450f5 --- /dev/null +++ b/src/pages/Gallary/ModalContext.jsx @@ -0,0 +1,23 @@ +import React, { createContext, useContext, useState } from "react"; + +const ModalContext = createContext(); + +export const ModalProvider1 = ({ children }) => { + const [modalContent, setModalContent] = useState(null); + + const openModal = (content) => setModalContent(content); + const closeModal = () => setModalContent(null); + + return ( + + {children} + {modalContent && ( +
+ {modalContent} +
+ )} +
+ ); +}; + +export const useModal = () => useContext(ModalContext);