From 99601095b5df4891187056a367bce1fdcff45c05 Mon Sep 17 00:00:00 2001 From: Kartik sharma Date: Wed, 2 Jul 2025 18:28:08 +0530 Subject: [PATCH 1/5] To show ImageGallery in our project. --- src/main.tsx | 3 + src/pages/Gallary/ImageGallary.jsx | 343 +++++++++++++++++++- src/pages/Gallary/ImageGallery.css | 429 ++++++++++++++++++++++++++ src/pages/Gallary/ImageGalleryAPI.jsx | 7 + src/pages/Gallary/ImagePop.css | 103 +++++++ src/pages/Gallary/ImagePop.jsx | 94 ++++++ src/pages/Gallary/ModalContext.jsx | 23 ++ 7 files changed, 997 insertions(+), 5 deletions(-) create mode 100644 src/pages/Gallary/ImageGallery.css create mode 100644 src/pages/Gallary/ImageGalleryAPI.jsx create mode 100644 src/pages/Gallary/ImagePop.css create mode 100644 src/pages/Gallary/ImagePop.jsx create mode 100644 src/pages/Gallary/ModalContext.jsx 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); -- 2.43.0 From a94facb062f6111409c041855015262461fbcd4f Mon Sep 17 00:00:00 2001 From: Kartik sharma Date: Thu, 3 Jul 2025 14:49:18 +0530 Subject: [PATCH 2/5] Changes in Directory filter adding clear and apply button and calling api at the time apply filter. --- src/pages/Gallary/ImageGallary.jsx | 380 +++++++++++------ src/pages/Gallary/ImageGallery.css | 590 +++++++++++++++----------- src/pages/Gallary/ImageGalleryAPI.jsx | 7 +- 3 files changed, 615 insertions(+), 362 deletions(-) diff --git a/src/pages/Gallary/ImageGallary.jsx b/src/pages/Gallary/ImageGallary.jsx index 5b3fca0d..ba023adb 100644 --- a/src/pages/Gallary/ImageGallary.jsx +++ b/src/pages/Gallary/ImageGallary.jsx @@ -1,11 +1,11 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef, useCallback } from "react"; import "./ImageGallery.css"; -import { ImageGalleryAPI } from "./ImageGalleryAPI"; // Assuming this exists +import { ImageGalleryAPI } from "./ImageGalleryAPI"; 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 +import { useSelector } from "react-redux"; +import { useModal } from "./ModalContext"; +import ImagePop from "./ImagePop"; +import Avatar from "../../components/common/Avatar"; const ImageGallery = () => { const [images, setImages] = useState([]); @@ -19,20 +19,41 @@ const ImageGallery = () => { uploadedBy: [], workCategory: [], workArea: [], + startDate: "", + endDate: "", }); - const [isFilterPanelOpen, setIsFilterPanelOpen] = useState(false); // Controls filter drawer - const [hoveredImage, setHoveredImage] = useState(null); // For image hover description + const [appliedFilters, setAppliedFilters] = useState({ + buildingIds: null, + floorIds: null, + activityIds: null, + uploadedByIds: null, + workCategoryIds: null, + workAreaIds: null, + startDate: null, + endDate: null, + }); + + const [collapsedFilters, setCollapsedFilters] = useState({ + dateRange: false, + building: false, + floor: false, + activity: false, + uploadedBy: false, + workCategory: false, + workArea: false, + }); + + const [isFilterPanelOpen, setIsFilterPanelOpen] = useState(false); + const [hoveredImage, setHoveredImage] = useState(null); 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 + const imageGroupRefs = useRef({}); + const filterPanelRef = useRef(null); + const filterButtonRef = useRef(null); - // 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) && @@ -49,136 +70,250 @@ const ImageGallery = () => { }; }, []); - // Fetch images when the selectedProjectId changes useEffect(() => { - if (!selectedProjectId) return; + if (!selectedProjectId) { + setImages([]); + setLoading(false); + return; + } setLoading(true); - ImageGalleryAPI.ImagesGet(selectedProjectId) + + ImageGalleryAPI.ImagesGet(selectedProjectId, appliedFilters) .then((res) => { setImages(res.data); }) .catch((err) => { console.error("Error fetching images:", err); + setImages([]); }) .finally(() => { setLoading(false); }); - }, [selectedProjectId]); + }, [selectedProjectId, appliedFilters]); - // 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)) + const getUniqueValuesWithIds = useCallback( + (idKey, nameKey) => { + const uniqueMap = new Map(); + images.forEach(img => { + if (img[idKey] && img[nameKey]) { + uniqueMap.set(img[idKey], img[nameKey]); + } + }); + return Array.from(uniqueMap.entries()); + }, + [images] + ); + + const getUniqueUploadedByUsers = useCallback( + () => { + const uniqueUsersMap = new Map(); + images.forEach(img => { + if (img.uploadedBy && img.uploadedBy.id) { + const fullName = `${img.uploadedBy.firstName || ""} ${img.uploadedBy.lastName || ""}`.trim(); + if (fullName) { + uniqueUsersMap.set(img.uploadedBy.id, fullName); + } + } + }); + return Array.from(uniqueUsersMap.entries()); + }, + [images] + ); + + const buildings = getUniqueValuesWithIds("buildingId", "buildingName"); + const floors = getUniqueValuesWithIds("floorIds", "floorName"); + const activities = getUniqueValuesWithIds("activityId", "activityName"); + const workAreas = getUniqueValuesWithIds("workAreaId", "workAreaName"); + const uploadedByUsers = getUniqueUploadedByUsers(); + const workCategories = getUniqueValuesWithIds("workCategoryId", "workCategoryName"); + + const toggleFilter = useCallback((type, itemId, itemName) => { + setSelectedFilters((prev) => { + const current = prev[type]; + const isSelected = current.some(item => item[0] === itemId); + + const newArray = isSelected + ? current.filter((item) => item[0] !== itemId) + : [...current, [itemId, itemName]]; + + return { + ...prev, + [type]: newArray, + }; + }); + }, []); + + const handleDateChange = useCallback((type, date) => { + setSelectedFilters((prev) => ({ + ...prev, + [type]: date, + })); + }, []); + + const toggleCollapse = useCallback((type) => { + setCollapsedFilters((prev) => ({ + ...prev, + [type]: !prev[type], + })); + }, []); + + const handleApplyFilters = useCallback(() => { + const payload = { + buildingIds: selectedFilters.building.length > 0 ? selectedFilters.building.map(item => item[0]) : null, + floorIds: selectedFilters.floor.length > 0 ? selectedFilters.floor.map(item => item[0]) : null, + workAreaIds: selectedFilters.workArea.length > 0 ? selectedFilters.workArea.map(item => item[0]) : null, + workCategoryIds: selectedFilters.workCategory.length > 0 ? selectedFilters.workCategory.map(item => item[0]) : null, + activityIds: selectedFilters.activity.length > 0 ? selectedFilters.activity.map(item => item[0]) : null, + uploadedByIds: selectedFilters.uploadedBy.length > 0 ? selectedFilters.uploadedBy.map(item => item[0]) : null, + startDate: selectedFilters.startDate || null, + endDate: selectedFilters.endDate || null, + }; + setAppliedFilters(payload); + setIsFilterPanelOpen(false); + }, [selectedFilters]); + + + const handleClearAllFilters = useCallback(() => { + const initialStateSelected = { + building: [], + floor: [], + activity: [], + uploadedBy: [], + workCategory: [], + workArea: [], + startDate: "", + endDate: "", + }; + setSelectedFilters(initialStateSelected); + + const initialStateApplied = { + buildingIds: null, + floorIds: null, + activityIds: null, + uploadedByIds: null, + workCategoryIds: null, + workAreaIds: null, + startDate: null, + endDate: null, + }; + setAppliedFilters(initialStateApplied); + + setIsFilterPanelOpen(false); + }, []); + + const filteredImages = images.filter( + (img) => { + const uploadedAtMoment = moment(img.uploadedAt); + const startDateMoment = appliedFilters.startDate ? moment(appliedFilters.startDate) : null; + const endDateMoment = appliedFilters.endDate ? moment(appliedFilters.endDate) : null; + + const isWithinDateRange = + (!startDateMoment || uploadedAtMoment.isSameOrAfter(startDateMoment, 'day')) && + (!endDateMoment || uploadedAtMoment.isSameOrBefore(endDateMoment, 'day')); + + const passesCategoryFilters = + (appliedFilters.buildingIds === null || appliedFilters.buildingIds.includes(img.buildingId)) && + (appliedFilters.floorIds === null || appliedFilters.floorIds.includes(img.floorIds)) && + (appliedFilters.activityIds === null || appliedFilters.activityIds.includes(img.activityId)) && + (appliedFilters.workAreaIds === null || appliedFilters.workAreaIds.includes(img.workAreaId)) && + (appliedFilters.uploadedByIds === null || appliedFilters.uploadedByIds.includes(img.uploadedBy?.id)) && + (appliedFilters.workCategoryIds === null || appliedFilters.workCategoryIds.includes(img.workCategoryId)); + + return isWithinDateRange && passesCategoryFilters; + } ); - // 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 userName = `${img.uploadedBy?.firstName || ""} ${img.uploadedBy?.lastName || "" + }`.trim(); + const workArea = img.workAreaName || "Unknown"; const key = `${img.activityName}__${userName}__${workArea}`; if (!imagesByActivityUser[key]) imagesByActivityUser[key] = []; imagesByActivityUser[key].push(img); }); - // Scroll functionality for horizontal image groups - const scrollLeft = (key) => { + const scrollLeft = useCallback((key) => { imageGroupRefs.current[key]?.scrollBy({ left: -200, behavior: "smooth" }); - }; + }, []); - const scrollRight = (key) => { + const scrollRight = useCallback((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 && ( +
+
toggleCollapse(type)}> + {label} +
+ {type === 'dateRange' && (selectedFilters.startDate || selectedFilters.endDate) && ( + )} + {type !== 'dateRange' && selectedFilters[type] && selectedFilters[type].length > 0 && ( + )}
-
- {items.map((item) => ( - - ))}
+ {!collapsedFilters[type] && ( +
+ {type === 'dateRange' ? ( +
+ + +
+ ) : ( + items.map((item) => { + const itemId = item[0]; + const itemName = item[1]; + const isChecked = selectedFilters[type].some(selectedItem => selectedItem[0] === itemId); + + return ( + + ); + }) + )} +
+ )}
); @@ -191,11 +326,8 @@ const ImageGallery = () => {
) : 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"); @@ -224,7 +356,6 @@ const ImageGallery = () => {
- {/* Location and Work Category display */}
{buildingName} > {floorName} > {workArea} >{" "} @@ -239,7 +370,6 @@ const ImageGallery = () => {
- {/* Left scroll arrow */}
- {/* Filter drawer section */}
+ {renderFilterCategory("Date Range", [], "dateRange")} {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 ImageGallery; - +export default ImageGallery; \ No newline at end of file diff --git a/src/pages/Gallary/ImageGallery.css b/src/pages/Gallary/ImageGallery.css index 1795d188..5fc79da0 100644 --- a/src/pages/Gallary/ImageGallery.css +++ b/src/pages/Gallary/ImageGallery.css @@ -1,25 +1,19 @@ .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). - */ + display: grid; grid-template-columns: 1fr 50px; - gap: 0px; /* Gap between main content and filter */ + gap: 4px; 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 */ + transition: grid-template-columns 0.3s ease-in-out; } .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%; @@ -46,19 +40,17 @@ 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 */ + position: relative; + scrollbar-width: none; + -ms-overflow-style: none; } .filter-drawer-wrapper::-webkit-scrollbar { - display: none; /* For Chrome, Safari, and Opera */ + display: none; } .filter-button { @@ -74,31 +66,26 @@ 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 */ + height: 40px; + width: 40px; + z-index: 100; } -/* 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 */ + width: calc(100% - 16px); + height: auto; + padding: 8px 12px; + border-radius: 6px 6px 0 0; + justify-content: space-between; } -/* 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 */ + padding: 0; + font-size: 20px; } .filter-button:hover { @@ -106,9 +93,10 @@ 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 */ + display: flex; + flex-direction: column; + gap: 8px; background-color: #fff; border: 1px solid #e5e7eb; border-radius: 6px; @@ -116,314 +104,442 @@ 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 */ + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + max-height: calc(100% - 40px); + padding-top: 37px; 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 */ + max-height: calc(100% - 40px); opacity: 1; transform: translateY(0); - /* Remove top radius to blend with button when open */ - border-top-left-radius: 0; - border-top-right-radius: 0; + overflow-y: auto; + /* padding-bottom: 8px; */ + /* Adjust padding to accommodate the new buttons */ + padding-bottom: -1px; /* Enough space for buttons + some padding */ } -/* ... (rest of your CSS remains the same) ... */ -/* Individual dropdown sections within the filter panel */ +.dropdown { + transition: all 0.3s ease-in-out; +} + .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; + display: block; + position: static; + box-shadow: none; + padding: 4px 10px; + border-radius: 0 0 4px 4px; + max-height: 150px; + overflow-y: auto; + transition: max-height 0.3s ease-in-out, padding 0.3s ease-in-out; +} + +.dropdown.collapsed .dropdown-content { + max-height: 0; + padding-top: 0; + padding-bottom: 0; + overflow: hidden; +} + +.dropdown-content::-webkit-scrollbar { + width: 6px; +} + +.dropdown-content::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 10px; +} + +.dropdown-content::-webkit-scrollbar-thumb { + background: #c0c0c0; + border-radius: 10px; +} + +.dropdown-content::-webkit-scrollbar-thumb:hover { + background: #a7a7a7; } .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; + display: flex; + align-items: center; + gap: 6px; + padding: 5px 0px; + font-size: 12px; + font-weight: 500; + color: #333; + cursor: pointer; + transition: background 0.2s; } .dropdown-content label:hover { - background-color: #eef2ff; + 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; + -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; + background-color: #6366f1; + border-color: #6366f1; } .dropdown-content input[type="checkbox"]:checked::after { - content: '✔'; - font-size: 10px; - color: white; - position: absolute; + 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; + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 10px; + background-color: #eef2ff; + border-bottom: 1px solid #c7d2fe; + font-weight: bold; + font-size: 13px; + color: #333; + cursor: pointer; + position: sticky; + top: 0; + z-index: 1; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + transition: background-color 0.3s ease, border-bottom 0.3s ease; +} + +.dropdown.collapsed .dropdown-header { + border-bottom: none; + border-radius: 4px; + box-shadow: none; } .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; + font-size: 10px; + background: none; + border: none; + color: #6366f1; + cursor: pointer; + padding: 3px 6px; + border-radius: 4px; + transition: background-color 0.2s ease; + flex-shrink: 0; } .clear-button:hover { - background-color: #eef2ff; + background-color: #eef2ff; +} + +.header-controls { + display: flex; + align-items: center; + gap: 8px; +} + +.toggle-icon { + font-size: 10px; + color: #6366f1; + transition: transform 0.2s ease; +} + +.dropdown .toggle-icon { + transform: rotate(0deg); +} + +.dropdown.collapsed .toggle-icon { + transform: rotate(-90deg); +} + +.date-range-inputs { + display: flex; + flex-direction: column; + gap: 10px; + padding: 5px 0; +} + +.date-range-inputs label { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + font-weight: 500; + color: #333; +} + +.date-input { + flex-grow: 1; + padding: 6px 8px; + border: 1px solid #c7d2fe; + border-radius: 4px; + font-size: 12px; + color: #333; + background-color: #fff; + outline: none; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.date-input:focus { + border-color: #6366f1; + box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2); } -/* --- 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); + 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; + 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; + margin-right: 15px; } .datetime-line { - font-size: 12px; - color: #777; - margin-top: 0px; - + 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; + font-weight: 600; + font-size: 12px; + color: #555; + text-align: right; + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.work-category-display { + margin-top: 4px; + padding: 2px 6px; + border-radius: 5px; + font-size: 12px; + color: #555; } -/* New: Wrapper for image group and arrows */ .image-group-wrapper { - position: relative; - display: flex; - align-items: center; - padding: 0 0px; + 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%; + 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; + background-color: rgba(0, 0, 0, 0.5); + color: white; + border: none; + border-radius: 50%; + width: 30px; + height: 44px; + 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; + opacity: 1; + pointer-events: auto; } .scroll-arrow:hover { - background-color: rgba(0, 0, 0, 0.7); + background-color: rgba(0, 0, 0, 0.7); } .left-arrow { - left: 5px; + left: 5px; } .right-arrow { - right: 5px; + 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; + 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; + display: none; } .image-card:hover { - transform: translateY(-2px) scale(1.03); - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); + 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; + 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; + 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); + opacity: 1; + transform: translateY(0); } .image-hover-description p { - margin: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .spinner-container { - display: flex; - justify-content: center; - align-items: center; - min-height: 200px; + 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; + 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); - } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } +/* New styles for filter action buttons */ +.filter-actions { + display: flex; + justify-content: space-between; + margin-top: auto; /* Pushes buttons to the bottom */ + padding-top: 10px; + border-top: 1px solid #e5e7eb; + background-color: #fff; + position: sticky; + bottom: 0; + z-index: 10; + padding-bottom: 0px; +} +.apply-filters-button, +.clear-all-button { + padding: 8px 15px; + border-radius: 5px; + font-size: 13px; + cursor: pointer; + transition: background-color 0.2s ease, box-shadow 0.2s ease; + flex: 1; /* Make buttons take equal width */ + margin: 0 4px; /* Add some spacing between them */ +} + +.apply-filters-button { + /* background-color: #6366f1; + color: white; */ + border: none; +} + +.apply-filters-button:hover { + background-color: #4f46e5; + /* box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); */ +} + +.clear-all-button { + border: 1px solid #cbd5e1; +} + +.clear-all-button:hover { + background-color: #e2e8f0; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); +} diff --git a/src/pages/Gallary/ImageGalleryAPI.jsx b/src/pages/Gallary/ImageGalleryAPI.jsx index 0c844b35..df18611d 100644 --- a/src/pages/Gallary/ImageGalleryAPI.jsx +++ b/src/pages/Gallary/ImageGalleryAPI.jsx @@ -2,6 +2,9 @@ import { api } from "../../utils/axiosClient"; export const ImageGalleryAPI = { - ImagesGet: (projectId) => - api.get(`/api/image/images/${projectId}`), + ImagesGet: (projectId, filter) => { + const payloadJsonString = JSON.stringify(filter); + console.log("Applying filters with payload JSON string:", payloadJsonString); + return api.get(`/api/image/images/${projectId}?filter=${payloadJsonString}`) + }, } \ No newline at end of file -- 2.43.0 From 09906420a6760677aae4ca241ca524a0673d2527 Mon Sep 17 00:00:00 2001 From: Kartik sharma Date: Fri, 4 Jul 2025 10:18:18 +0530 Subject: [PATCH 3/5] Adding Date Range picker in filter dropdown. --- src/pages/Gallary/ImageGallary.jsx | 245 +++++++++++++++----------- src/pages/Gallary/ImageGallery.css | 64 ++++--- src/pages/Gallary/ImageGalleryAPI.jsx | 1 - 3 files changed, 179 insertions(+), 131 deletions(-) diff --git a/src/pages/Gallary/ImageGallary.jsx b/src/pages/Gallary/ImageGallary.jsx index ba023adb..da51b496 100644 --- a/src/pages/Gallary/ImageGallary.jsx +++ b/src/pages/Gallary/ImageGallary.jsx @@ -1,3 +1,4 @@ +// ImageGallery.js import React, { useState, useEffect, useRef, useCallback } from "react"; import "./ImageGallery.css"; import { ImageGalleryAPI } from "./ImageGalleryAPI"; @@ -6,12 +7,15 @@ import { useSelector } from "react-redux"; import { useModal } from "./ModalContext"; import ImagePop from "./ImagePop"; import Avatar from "../../components/common/Avatar"; +import DateRangePicker from "../../components/common/DateRangePicker"; // Assuming this is the path to your DateRangePicker const ImageGallery = () => { const [images, setImages] = useState([]); const selectedProjectId = useSelector((store) => store.localVariables.projectId); const { openModal } = useModal(); + const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD'); + const [selectedFilters, setSelectedFilters] = useState({ building: [], floor: [], @@ -95,7 +99,7 @@ const ImageGallery = () => { const getUniqueValuesWithIds = useCallback( (idKey, nameKey) => { const uniqueMap = new Map(); - images.forEach(img => { + images.forEach((img) => { if (img[idKey] && img[nameKey]) { uniqueMap.set(img[idKey], img[nameKey]); } @@ -105,21 +109,20 @@ const ImageGallery = () => { [images] ); - const getUniqueUploadedByUsers = useCallback( - () => { - const uniqueUsersMap = new Map(); - images.forEach(img => { - if (img.uploadedBy && img.uploadedBy.id) { - const fullName = `${img.uploadedBy.firstName || ""} ${img.uploadedBy.lastName || ""}`.trim(); - if (fullName) { - uniqueUsersMap.set(img.uploadedBy.id, fullName); - } + const getUniqueUploadedByUsers = useCallback(() => { + const uniqueUsersMap = new Map(); + images.forEach((img) => { + if (img.uploadedBy && img.uploadedBy.id) { + const fullName = `${img.uploadedBy.firstName || ""} ${ + img.uploadedBy.lastName || "" + }`.trim(); + if (fullName) { + uniqueUsersMap.set(img.uploadedBy.id, fullName); } - }); - return Array.from(uniqueUsersMap.entries()); - }, - [images] - ); + } + }); + return Array.from(uniqueUsersMap.entries()); + }, [images]); const buildings = getUniqueValuesWithIds("buildingId", "buildingName"); const floors = getUniqueValuesWithIds("floorIds", "floorName"); @@ -131,7 +134,7 @@ const ImageGallery = () => { const toggleFilter = useCallback((type, itemId, itemName) => { setSelectedFilters((prev) => { const current = prev[type]; - const isSelected = current.some(item => item[0] === itemId); + const isSelected = current.some((item) => item[0] === itemId); const newArray = isSelected ? current.filter((item) => item[0] !== itemId) @@ -144,10 +147,11 @@ const ImageGallery = () => { }); }, []); - const handleDateChange = useCallback((type, date) => { + const setDateRange = useCallback(({ startDate, endDate }) => { setSelectedFilters((prev) => ({ ...prev, - [type]: date, + startDate: startDate || "", + endDate: endDate || "", })); }, []); @@ -160,12 +164,30 @@ const ImageGallery = () => { const handleApplyFilters = useCallback(() => { const payload = { - buildingIds: selectedFilters.building.length > 0 ? selectedFilters.building.map(item => item[0]) : null, - floorIds: selectedFilters.floor.length > 0 ? selectedFilters.floor.map(item => item[0]) : null, - workAreaIds: selectedFilters.workArea.length > 0 ? selectedFilters.workArea.map(item => item[0]) : null, - workCategoryIds: selectedFilters.workCategory.length > 0 ? selectedFilters.workCategory.map(item => item[0]) : null, - activityIds: selectedFilters.activity.length > 0 ? selectedFilters.activity.map(item => item[0]) : null, - uploadedByIds: selectedFilters.uploadedBy.length > 0 ? selectedFilters.uploadedBy.map(item => item[0]) : null, + buildingIds: + selectedFilters.building.length > 0 + ? selectedFilters.building.map((item) => item[0]) + : null, + floorIds: + selectedFilters.floor.length > 0 + ? selectedFilters.floor.map((item) => item[0]) + : null, + workAreaIds: + selectedFilters.workArea.length > 0 + ? selectedFilters.workArea.map((item) => item[0]) + : null, + workCategoryIds: + selectedFilters.workCategory.length > 0 + ? selectedFilters.workCategory.map((item) => item[0]) + : null, + activityIds: + selectedFilters.activity.length > 0 + ? selectedFilters.activity.map((item) => item[0]) + : null, + uploadedByIds: + selectedFilters.uploadedBy.length > 0 + ? selectedFilters.uploadedBy.map((item) => item[0]) + : null, startDate: selectedFilters.startDate || null, endDate: selectedFilters.endDate || null, }; @@ -173,7 +195,6 @@ const ImageGallery = () => { setIsFilterPanelOpen(false); }, [selectedFilters]); - const handleClearAllFilters = useCallback(() => { const initialStateSelected = { building: [], @@ -202,32 +223,41 @@ const ImageGallery = () => { setIsFilterPanelOpen(false); }, []); - const filteredImages = images.filter( - (img) => { - const uploadedAtMoment = moment(img.uploadedAt); - const startDateMoment = appliedFilters.startDate ? moment(appliedFilters.startDate) : null; - const endDateMoment = appliedFilters.endDate ? moment(appliedFilters.endDate) : null; + const filteredImages = images.filter((img) => { + const uploadedAtMoment = moment(img.uploadedAt); + const startDateMoment = appliedFilters.startDate + ? moment(appliedFilters.startDate) + : null; + const endDateMoment = appliedFilters.endDate + ? moment(appliedFilters.endDate) + : null; - const isWithinDateRange = - (!startDateMoment || uploadedAtMoment.isSameOrAfter(startDateMoment, 'day')) && - (!endDateMoment || uploadedAtMoment.isSameOrBefore(endDateMoment, 'day')); + const isWithinDateRange = + (!startDateMoment || uploadedAtMoment.isSameOrAfter(startDateMoment, "day")) && + (!endDateMoment || uploadedAtMoment.isSameOrBefore(endDateMoment, "day")); - const passesCategoryFilters = - (appliedFilters.buildingIds === null || appliedFilters.buildingIds.includes(img.buildingId)) && - (appliedFilters.floorIds === null || appliedFilters.floorIds.includes(img.floorIds)) && - (appliedFilters.activityIds === null || appliedFilters.activityIds.includes(img.activityId)) && - (appliedFilters.workAreaIds === null || appliedFilters.workAreaIds.includes(img.workAreaId)) && - (appliedFilters.uploadedByIds === null || appliedFilters.uploadedByIds.includes(img.uploadedBy?.id)) && - (appliedFilters.workCategoryIds === null || appliedFilters.workCategoryIds.includes(img.workCategoryId)); + const passesCategoryFilters = + (appliedFilters.buildingIds === null || + appliedFilters.buildingIds.includes(img.buildingId)) && + (appliedFilters.floorIds === null || + appliedFilters.floorIds.includes(img.floorIds)) && + (appliedFilters.activityIds === null || + appliedFilters.activityIds.includes(img.activityId)) && + (appliedFilters.workAreaIds === null || + appliedFilters.workAreaIds.includes(img.workAreaId)) && + (appliedFilters.uploadedByIds === null || + appliedFilters.uploadedByIds.includes(img.uploadedBy?.id)) && + (appliedFilters.workCategoryIds === null || + appliedFilters.workCategoryIds.includes(img.workCategoryId)); - return isWithinDateRange && passesCategoryFilters; - } - ); + return isWithinDateRange && passesCategoryFilters; + }); const imagesByActivityUser = {}; filteredImages.forEach((img) => { - const userName = `${img.uploadedBy?.firstName || ""} ${img.uploadedBy?.lastName || "" - }`.trim(); + const userName = `${img.uploadedBy?.firstName || ""} ${ + img.uploadedBy?.lastName || "" + }`.trim(); const workArea = img.workAreaName || "Unknown"; const key = `${img.activityName}__${userName}__${workArea}`; if (!imagesByActivityUser[key]) imagesByActivityUser[key] = []; @@ -243,62 +273,48 @@ const ImageGallery = () => { }, []); const renderFilterCategory = (label, items, type) => ( -
+
toggleCollapse(type)}> {label}
- {type === 'dateRange' && (selectedFilters.startDate || selectedFilters.endDate) && ( - - )} - {type !== 'dateRange' && selectedFilters[type] && selectedFilters[type].length > 0 && ( - - )} + {type === "dateRange" && (selectedFilters.startDate || selectedFilters.endDate) && ( + + )} + {type !== "dateRange" && + selectedFilters[type] && + selectedFilters[type].length > 0 && ( + + )}
{!collapsedFilters[type] && (
- {type === 'dateRange' ? ( -
- - -
+ {type === "dateRange" ? ( + // The DateRangePicker will be rendered outside this function, at the end of the filter panel + null ) : ( items.map((item) => { const itemId = item[0]; const itemName = item[1]; - const isChecked = selectedFilters[type].some(selectedItem => selectedItem[0] === itemId); + const isChecked = selectedFilters[type].some( + (selectedItem) => selectedItem[0] === itemId + ); return (