diff --git a/src/components/Directory/NoteCardDirectoryEditable.jsx b/src/components/Directory/NoteCardDirectoryEditable.jsx index 7759e62a..de91ad69 100644 --- a/src/components/Directory/NoteCardDirectoryEditable.jsx +++ b/src/components/Directory/NoteCardDirectoryEditable.jsx @@ -184,7 +184,7 @@ const NoteCardDirectoryEditable = ({ ) : ( diff --git a/src/data/menuData.json b/src/data/menuData.json index 0802133b..cadf34bf 100644 --- a/src/data/menuData.json +++ b/src/data/menuData.json @@ -46,11 +46,7 @@ "available": true, "link": "/activities/reports" }, - { - "text": "Image Gallary", - "available": true, - "link": "/activities/gallary" - }, + { "text": "Daily Expenses", "available": true, @@ -63,6 +59,12 @@ "icon": "bx bx-group", "available": true, "link": "/directory" + }, + { + "text": "Image Gallary", + "icon": "bx bx-images", + "available": true, + "link": "/gallary" }, { "text": "Administration", 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/Directory/DirectoryPageHeader.jsx b/src/pages/Directory/DirectoryPageHeader.jsx index 6a3ae418..4efc043e 100644 --- a/src/pages/Directory/DirectoryPageHeader.jsx +++ b/src/pages/Directory/DirectoryPageHeader.jsx @@ -278,7 +278,7 @@ const DirectoryPageHeader = ({
-
+
{ - return ; +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: [], + activity: [], + uploadedBy: [], + workCategory: [], + workArea: [], + startDate: "", + endDate: "", + }); + + 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({}); + const filterPanelRef = useRef(null); + const filterButtonRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event) => { + 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); + }; + }, []); + + useEffect(() => { + if (!selectedProjectId) { + setImages([]); + setLoading(false); + return; + } + + setLoading(true); + + ImageGalleryAPI.ImagesGet(selectedProjectId, appliedFilters) + .then((res) => { + setImages(res.data); + }) + .catch((err) => { + console.error("Error fetching images:", err); + setImages([]); + }) + .finally(() => { + setLoading(false); + }); + }, [selectedProjectId, appliedFilters]); + + 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 setDateRange = useCallback(({ startDate, endDate }) => { + setSelectedFilters((prev) => ({ + ...prev, + startDate: startDate || "", + endDate: endDate || "", + })); + }, []); + + 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); + }, []); + + 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; + }); + + const imagesByActivityUser = {}; + filteredImages.forEach((img) => { + 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); + }); + + const scrollLeft = useCallback((key) => { + imageGroupRefs.current[key]?.scrollBy({ left: -200, behavior: "smooth" }); + }, []); + + const scrollRight = useCallback((key) => { + imageGroupRefs.current[key]?.scrollBy({ left: 200, behavior: "smooth" }); + }, []); + + const renderFilterCategory = (label, items, type) => ( +
+
toggleCollapse(type)}> + {label} +
+ {type === "dateRange" && (selectedFilters.startDate || selectedFilters.endDate) && ( + + )} + {type !== "dateRange" && + selectedFilters[type] && + selectedFilters[type].length > 0 && ( + + )} + + {collapsedFilters[type] ? '+' : '-'} + +
+
+ {!collapsedFilters[type] && ( +
+ {type === "dateRange" ? ( + null + ) : ( + items.map((item) => { + const itemId = item[0]; + const itemName = item[1]; + const isChecked = selectedFilters[type].some( + (selectedItem) => selectedItem[0] === itemId + ); + + return ( + + ); + }) + )} +
+ )} +
+ ); + + return ( +
+
+
+ {loading ? ( +
+
+
+ ) : Object.entries(imagesByActivityUser).length > 0 ? ( + Object.entries(imagesByActivityUser).map(([key, imgs]) => { + const [activity, userName, workArea] = key.split("__"); + 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} + +
+
+
+ +
+
+ {buildingName} > {floorName} > {workArea} >{" "} + {activity} +
+ {workCategoryName && ( +
+ + {workCategoryName} + +
+ )} +
+
+ +
+ +
(imageGroupRefs.current[key] = el)} + > + {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 +
+ {hoveredImage === img && ( +
+

+ Date: {hoverDate} +

+

+ Time: {hoverTime} +

+

+ Activity: {img.activityName} +

+
+ )} +
+ ); + })} +
+ +
+
+ ); + }) + ) : ( +

+ No images match the selected filters. +

+ )} +
+
+ +
+ +
+
+
toggleCollapse('dateRange')}> + Date Range + + + {collapsedFilters.dateRange ? '+' : '-'} + +
+ {!collapsedFilters.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 ImageGallary; +export default ImageGallery; \ No newline at end of file diff --git a/src/pages/Gallary/ImageGallery.css b/src/pages/Gallary/ImageGallery.css new file mode 100644 index 00000000..377fe531 --- /dev/null +++ b/src/pages/Gallary/ImageGallery.css @@ -0,0 +1,546 @@ +/* ImageGallery.css */ +.gallery-container { + display: grid; + grid-template-columns: 1fr 50px; + 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; +} + +.gallery-container.filter-panel-open { + grid-template-columns: 1fr 350px; +} + +.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; +} + +.filter-drawer-wrapper { + flex-shrink: 0; + max-height: 100%; + box-sizing: border-box; + position: relative; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.filter-drawer-wrapper::-webkit-scrollbar { + display: none; +} + +.filter-button { + 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); + 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; + position: absolute; + top: 0; + right: 0; + height: 40px; + width: 40px; + z-index: 100; +} + +.gallery-container.filter-panel-open .filter-button { + width: calc(100% - 1px); + height: auto; + padding: 8px 12px; + border-radius: 6px 6px 0 0; + justify-content: space-between; +} + +.filter-button.closed-icon { + padding: 0; + font-size: 20px; +} + +.filter-button:hover { + background-color: #4f46e5; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.filter-panel { + display: flex; + flex-direction: column; + gap: 8px; + 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; + 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); + transition: max-height 0.3s ease-out, opacity 0.3s ease-out, + transform 0.3s ease-out, border-radius 0.3s ease-in-out; +} + +.filter-panel.open { + max-height: calc(100% - 0px); + opacity: 1; + transform: translateY(0); + overflow-y: auto; + padding-bottom: 0px; + /* Adjusted padding-bottom */ +} + +.dropdown { + transition: all 0.3s ease-in-out; +} + +.dropdown-content { + display: block; + position: static; + box-shadow: none; + padding: 4px 10px; + border-radius: 0 0 4px 4px; + max-height: 150px; + /* Default max-height for scrollable dropdowns */ + overflow-y: auto; + /* Default overflow for scrollable dropdowns */ + 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; +} + +/* --- New styles for Date Range Picker dropdown --- */ +.dropdown strong:contains("Date Range") + .header-controls + .dropdown-content { + max-height: none; + /* Remove max-height */ + overflow-y: visible; + /* Allow content to dictate height */ +} + +.dropdown strong:contains("Date Range") + .header-controls + .dropdown-content::-webkit-scrollbar { + display: none; + /* Hide scrollbar for Webkit browsers */ +} + +/* --- End new styles --- */ + +.dropdown-content label { + 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; +} + +.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: 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; + flex-shrink: 0; +} + +.clear-button:hover { + background-color: #eef2ff; +} + +.header-controls { + display: flex; + align-items: center; + gap: 8px; +} + +/* New style for the collapse icon */ +.collapse-icon { + font-size: 16px; /* Adjust size as needed */ + color: #6366f1; + margin-left: 8px; /* Space between clear button and icon */ + transition: transform 0.2s ease; +} + +.dropdown.collapsed .collapse-icon { + transform: rotate(0deg); /* No rotation for collapsed */ +} + +/* Original toggle-icon styles (if they still exist and are needed) */ +.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); +} + +.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-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; +} + +.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%; + 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; +} + +.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; +} + +.image-card:hover { + transform: translateY(-2px) scale(1.03); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); +} +hr { + display: none; +} + + +.image-wrapper img { + width: 100%; + height: 100px; + object-fit: cover; + display: block; + border-radius: 8px 8px 0 0; +} + +.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); + } +} + +.filter-actions { + display: flex; + justify-content: end; + margin-top: auto; + padding-top: 10px; + position: sticky; + bottom: 0; + z-index: 10; + padding-bottom: 5px; + gap: 8px; +} + +.datepicker { + margin-right: 135px; + margin-top: 6px; +} \ No newline at end of file diff --git a/src/pages/Gallary/ImageGalleryAPI.jsx b/src/pages/Gallary/ImageGalleryAPI.jsx new file mode 100644 index 00000000..13e6a56a --- /dev/null +++ b/src/pages/Gallary/ImageGalleryAPI.jsx @@ -0,0 +1,9 @@ +import { api } from "../../utils/axiosClient"; + +export const ImageGalleryAPI = { + + ImagesGet: (projectId, filter) => { + const payloadJsonString = JSON.stringify(filter); + return api.get(`/api/image/images/${projectId}?filter=${payloadJsonString}`) + }, +} \ 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); diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx index eac7d762..7c364ca9 100644 --- a/src/router/AppRoutes.jsx +++ b/src/router/AppRoutes.jsx @@ -75,7 +75,7 @@ const router = createBrowserRouter( { path: "/activities/records/:projectId?", element: }, { path: "/activities/task", element: }, { path: "/activities/reports", element: }, - { path: "/activities/gallary", element: }, + { path: "/gallary", element: }, { path: "/masters", element: }, { path: "/help/support", element: }, { path: "/help/docs", element: },