From 1ab54847d5f6bdaf9025b6d324eac25722c7475b Mon Sep 17 00:00:00 2001 From: Khanh Bui Date: Sat, 26 Oct 2024 19:43:13 -0600 Subject: [PATCH 1/2] Added alot (description below) App.tsx: change routes for MemberPage Gallery.tsx: I forgot MediaGrid.tsx: change the return to get parent size, added sort order based on time MemberHeader.tsx: change tailwindcss to match NavigationBar.tsx: Correct signout logic SortDropdownList.tsx: change from multiple sort setting to only 2 CaregiverLogin.tsx: add localstorage for caregiver login MemberPage.tsx: Add member page --- src/App.tsx | 7 +- src/components/Gallery.tsx | 153 +++--------------- src/components/MediaGrid.tsx | 33 +++- src/components/MemberHeader.tsx | 2 +- src/components/NavigationBar.tsx | 22 +-- src/components/SortDropdownList.tsx | 109 ++++++------- src/routes/MemberPage.tsx | 201 ++++++++++++++++++++++++ src/routes/caregiver/CaregiverLogin.tsx | 3 + 8 files changed, 315 insertions(+), 215 deletions(-) create mode 100644 src/routes/MemberPage.tsx diff --git a/src/App.tsx b/src/App.tsx index 4d5f898..aa706ba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,9 +31,10 @@ import MemberPageBackgroundTestRoute from "./routes/test/MemberPageBackgroundTes import AdminLogin from "./routes/admin/AdminLogin"; import AdminHome from "./routes/admin/AdminHome"; // import Admin - import CaregiverLogin from "./routes/caregiver/CaregiverLogin"; import MediaCardTestRoute from "./routes/test/MediaCardTestRoute"; +// User page +import MemberPage from "./routes/MemberPage"; const router = createBrowserRouter([ { @@ -144,6 +145,10 @@ const router = createBrowserRouter([ path: "/member-background-test", element: , }, + { + path: "/", + element: , + }, // Admin Routes { diff --git a/src/components/Gallery.tsx b/src/components/Gallery.tsx index f66bf23..d43b0e5 100644 --- a/src/components/Gallery.tsx +++ b/src/components/Gallery.tsx @@ -1,110 +1,29 @@ -import React, { useState, useEffect } from "react"; +//#region Imports import { CaretLeft } from "@phosphor-icons/react"; import MediaUploadZone from "./MediaUploadZone"; -import { getStorage, ref, uploadBytesResumable, listAll, getDownloadURL } from "firebase/storage"; -import Button from "@/components/Button"; -import MediaUploadStatus from "./MediaUploadStatus"; - -interface FileUploadStatus { - file: File; - progress: number; -} +import picture from "@/assets/images/pic1.jpg"; +//#endregion function Gallery() { - const [uploads, setUploads] = useState([]); - const [imgList, setImgList] = useState([]); - - // Create a root reference - const storage = getStorage(); + //TODO: Configure the local gallery to display images here + const imgList: string[] = [picture, picture, picture, picture, picture]; - /** - * Extract current pictures in /images inside firebase storage - */ - const displayImages = async () => { - const listRef = ref(storage, "/images"); - try { - const res = await listAll(listRef); - const imageUrls = await Promise.all( - res.items.map((itemRef) => getDownloadURL(itemRef)) - ); - setImgList(imageUrls); - } catch (error) { - console.error("Error displaying images: ", error); - } - }; + //#region Functions /** * Close the gallery */ const handleClose = () => {}; - // Useeffect to call the function to fetch images on component mount - useEffect(() => { - displayImages(); // - }, []); - /** * Handle an image in the gallery being clicked */ const handleImageClicked = () => {}; - // Function to handle adding files and simulate 1-second upload process - const handleFilesAdded = (files: File[]) => { - files.forEach((file) => { - // Create a new upload status object - const newUpload: FileUploadStatus = { - file, - progress: 0, - }; - - // Add the new upload to the uploads state - setUploads((prevUploads) => [...prevUploads, newUpload]); - - // Create a reference to where the file will be stored - const fileRef = ref(storage, `images/${file.name}`); - - // Create a file upload task - const uploadTask = uploadBytesResumable(fileRef, file); - - // Monitor the file upload progress - uploadTask.on( - "state_changed", - (snapshot) => { - const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100; - console.log(`Upload is ${progress}% done`); - - // Update the progress state for the UI - setUploads((prevUploads) => - prevUploads.map((prevUpload) => - prevUpload.file.name === file.name - ? { ...prevUpload, progress } - : prevUpload - ) - ); - }, - (error) => { - // Handle errors - console.error("Upload failed", error); - }, - async () => { - try { - const downloadURL = await getDownloadURL(fileRef); - console.log("File available at", downloadURL); - - // Add the download URL to the image list - setImgList((prevImgList) => [...prevImgList, downloadURL]); - - - } catch (error) { - console.error("Error getting download URL:", error); - } - } - ); - }); - }; + //#endregion return ( -
+
{/* Title */}
@@ -118,20 +37,16 @@ function Gallery() {
{/* Image gallery */} -
- {imgList.length > 0 ? ( - imgList.map((img, index) => ( - gallery - )) - ) : ( -

No images available

- )} +
+ {imgList.map((img, index) => ( + gallery + ))}
{/* Or */} @@ -143,37 +58,7 @@ function Gallery() { {/* Upload media */}

Upload Media

- - - {/* Display upload status */} -
- {uploads.map((upload, index) => ( - {}} - /> - ))} -
- - {/* Buttons */} -
-
+
); } diff --git a/src/components/MediaGrid.tsx b/src/components/MediaGrid.tsx index f78ca16..0da1de9 100644 --- a/src/components/MediaGrid.tsx +++ b/src/components/MediaGrid.tsx @@ -1,19 +1,36 @@ import { Masonry } from "masonic"; import MediaCard from "./MediaCard"; +interface Media { + src: string; + caption: string; + id: number; + date: Date; +} + interface MediaGridProps { data: Media[]; + sortOrder: "latest" | "oldest"; // Sorting order as a prop } -function MediaGrid({ data }: MediaGridProps) { +function MediaGrid({ data, sortOrder }: MediaGridProps) { + // Sort data based on sortOrder + const sortedData = [...data].sort((a, b) => { + return sortOrder === "latest" + ? b.date.getTime() - a.date.getTime() // Descending for latest + : a.date.getTime() - b.date.getTime(); // Ascending for oldest + }); + return ( - +
+ +
); } diff --git a/src/components/MemberHeader.tsx b/src/components/MemberHeader.tsx index 26c80f9..c5a8341 100644 --- a/src/components/MemberHeader.tsx +++ b/src/components/MemberHeader.tsx @@ -31,7 +31,7 @@ function MemberHeader({ {profilePicChildren}
-
+

{username}

diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx index 2238fa3..9bfe7c4 100644 --- a/src/components/NavigationBar.tsx +++ b/src/components/NavigationBar.tsx @@ -42,27 +42,27 @@ export function NavigationBar({ //#region Functions function signOut() { // Sign out logic for user - if ( - localStorage.getItem("lastName") && - localStorage.getItem("passcode") - ) { + if (userType === "user") { + localStorage.removeItem("lastName"); localStorage.removeItem("passcode"); displayToast("Successfully signed out", "success"); setTimeout(() => { navigate("/login"); }, 2000); - } - // Sign out logic for admin - auth.signOut().then(() => { - displayToast("Successfully signed out", "success"); - setTimeout(() => { + } else if (userType === "admin") { + + // Sign out logic for admin + auth.signOut().then(() => { + displayToast("Successfully signed out", "success"); + setTimeout(() => { + navigate("/admin/login"); + }, 2000); navigate("/admin/login"); - }, 2000); - navigate("/admin/login"); }); } +} //#endregion return ( diff --git a/src/components/SortDropdownList.tsx b/src/components/SortDropdownList.tsx index 575d421..531984a 100644 --- a/src/components/SortDropdownList.tsx +++ b/src/components/SortDropdownList.tsx @@ -1,82 +1,71 @@ -import { useEffect, useRef, useState } from "react"; -import DropdownItem from "./DropdownItem"; import { CaretDown } from "@phosphor-icons/react"; +import { useEffect, useRef, useState } from "react"; -function SortDropdownList() { - // Options that can be later add in - const items = [ - "Newest to Oldest", - "Oldest to Newest", - "Most-Least Storage", - "Recently Updated", - ]; - - // State to keep track of the currently selected option - const [selectedItem, setSelectedItem] = useState( - null - ); // useState expect either string or null var - - // State to keep track of the menu is being open or not - const [open, setOpen] = useState(false); +interface SortDropdownProps { + onSelect: (sortOrder: string) => void; // Callback prop for selecting sort order +} - // Check if user click outside of the menu to close it - let menuRef = useRef(null); // Typescript like this +function SortDropdown({ onSelect }: SortDropdownProps) { + // State to manage the dropdown open/closed state + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + // State to keep track of the current sort order + const [sortOrder, setSortOrder] = useState("latest"); + // Ref to handle clicks outside the dropdown + const dropdownRef = useRef(null); useEffect(() => { - let handler = (e: any) => { - if (menuRef.current && !menuRef.current.contains(e.target)) { - // Check menuRef.current is not null before calling .contains - setOpen(false); + // Handler to close dropdown when clicking outside of it + const handler = (e: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { + setIsDropdownOpen(false); } }; + // Add event listener for mouse down document.addEventListener("mousedown", handler); - return () => { - // Clean up to prevent memory leaks and avoid running outdated event handlers + // Clean up the event listener on component unmount document.removeEventListener("mousedown", handler); }; }, []); + // Function to handle selection of sort order + const handleSortSelect = (order: string) => { + setSortOrder(order); // Update the sort order + onSelect(order); // Call the passed in prop to update sort order + setIsDropdownOpen(false); // Close the dropdown after selection + }; + return ( -
- {/* For the menu trigger */} -
setOpen(!open)} +
+ {/* Dropdown Button */} +
+ Sort + + - {/* For the dropdown options */} -
-
-
    - {items.map((item, index) => ( - setSelectedItem(item)} // set the selectedItem to that option and when it re-render it will be check by the line above - /> - ))} -
- {/* for fun */} - + {/* Dropdown Menu */} + {isDropdownOpen && ( +
+
handleSortSelect("latest")} // Sort by latest + > + Latest +
+
handleSortSelect("oldest")} // Sort by oldest + > + Oldest +
-
+ )}
); } -export default SortDropdownList; +export default SortDropdown; diff --git a/src/routes/MemberPage.tsx b/src/routes/MemberPage.tsx new file mode 100644 index 0000000..5218ece --- /dev/null +++ b/src/routes/MemberPage.tsx @@ -0,0 +1,201 @@ +import React, { useEffect, useState } from 'react'; +import { ToastContainer } from "react-toastify"; +import { NavigationBar } from "@/components/NavigationBar"; +import MediaGrid from "@/components/MediaGrid"; +import MemberHeader from "@/components/MemberHeader"; +import { useNavigate } from "react-router-dom"; +import profilePic from "@/assets/images/face2.jpg"; +import {CaretDown } from "@phosphor-icons/react"; +import SortDropdownList from "@/components/SortDropdownList"; +import { + getFirestore, + collection, + getDocs, + query, + where, + DocumentData + +} from "firebase/firestore"; +import { initializeApp } from "firebase/app"; + + +// Temporary data for media grid +const data = [ + // Array of media items with image source, caption, ID, and date + + { + src: "https://images.unsplash.com/photo-1724579243894-6a8c9bbfe88c", + caption: + "Channeling their inner artist with a paintbrush and a splash of color!", + id: 1234, + date: new Date("Oct 5, 2024"), + }, + { + src: "https://images.unsplash.com/photo-1723984834599-5357b87f727c", + caption: + "Channeling their inner artist with a paintbrush and a splash of color!", + id: 1235, + date: new Date("Oct 4, 2024"), + }, + { + src: "https://images.unsplash.com/photo-1724505599369-2c1d43324fdc", + caption: + "Channeling their inner artist with a paintbrush and a splash of color!", + id: 1236, + date: new Date("Oct 3, 2024"), + }, + { + src: "https://images.unsplash.com/photo-1724579243894-6a8c9bbfe88c", + caption: + "Channeling their inner artist with a paintbrush and a splash of color!", + id: 1237, + date: new Date("Oct 7, 2024"), + }, + { + src: "https://images.unsplash.com/photo-1723984834599-5357b87f727c", + caption: + "Channeling their inner artist with a paintbrush and a splash of color!", + id: 1238, + date: new Date("Oct 8, 2024"), + }, + { + src: "https://images.unsplash.com/photo-1724505599369-2c1d43324fdc", + caption: + "Channeling their inner artist with a paintbrush and a splash of color!", + id: 1239, + date: new Date("Oct 2, 2024"), + }, + { + src: "https://images.unsplash.com/photo-1724579243894-6a8c9bbfe88c", + caption: + "Channeling their inner artist with a paintbrush and a splash of color!", + id: 1240, + date: new Date("Oct 2, 2024"), + }, + { + src: "https://images.unsplash.com/photo-1723984834599-5357b87f727c", + caption: + "Channeling their inner artist with a paintbrush and a splash of color!", + id: 1241, + date: new Date("Oct 2, 2024"), + }, + { + src: "https://images.unsplash.com/photo-1724505599369-2c1d43324fdc", + caption: + "Channeling their inner artist with a paintbrush and a splash of color!", + id: 1242, + date: new Date("Oct 9, 2024"), + }, + { + src: "https://images.unsplash.com/photo-1724579243894-6a8c9bbfe88c", + caption: + "Channeling their inner artist with a paintbrush and a splash of color!", + id: 1240, + date: new Date("Oct 10, 2024"), + }, + { + src: "https://images.unsplash.com/photo-1723984834599-5357b87f727c", + caption: + "Channeling their inner artist with a paintbrush and a splash of color!", + id: 1241, + date: new Date("Oct 2, 2024"), + }, + { + src: "https://images.unsplash.com/photo-1724505599369-2c1d43324fdc", + caption: + "Channeling their inner artist with a paintbrush and a splash of color!", + id: 1242, + date: new Date("Oct 2, 2024"), + }, +]; + + +//#region firebase +const firebaseConfig = JSON.parse(import.meta.env.VITE_FIREBASE_CONFIG); +const app = initializeApp(firebaseConfig); +const database = getFirestore(app); +const usersRef = collection(database, "users"); +//#endregion + +export default function MemberPage() { + const navigate = useNavigate(); + const [sortOrder, setSortOrder] = useState<'latest' | 'oldest'>('latest'); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [userData, setUserData] = useState(null); + + + type SortOrder = "latest" | "oldest"; + + // Check for user login info in localStorage and fetch user data from Firebase + useEffect(() => { + const fetchData = async () => { + const lastName = localStorage.getItem("lastName"); + const passcode = localStorage.getItem("passcode"); + if (!lastName || !passcode) { + // Redirect to login page if lastName or passcode is missing + navigate("/login"); + return; + } + // Firebase query to find user with matching lastName and passcode + const q = query( + collection(database, "users"), + where("lastName", "==", lastName), + where("passcode", "==", passcode) + ); + + const querySnapshot = await getDocs(q); + if (!querySnapshot.empty) { + const userDoc = querySnapshot.docs[0].data(); // Set retrieved user data to userData state + setUserData(userDoc); + } else { + navigate("/login"); // Redirect to login if no user matches + } + }; + + fetchData(); + }, [navigate]); + + // Function to update sortOrder and close the dropdown menu + const handleSortSelect = (order:SortOrder) => { + setSortOrder(order); // Update sort order (latest or oldest) + setIsDropdownOpen(false); // Close dropdown after selection + }; + + return ( +
+ {/* Toast notification container */} + + {/* Navigation bar for the user type "user" */} + +
+ {/* Display user header with profile picture and username if userData is available */} +
+ {userData && ( + + } + /> + )} +
+ {/* SortDropdownList component for choosing sort order */} +
+
+ {/* temporary */} + +
+ {/* MediaGrid component to display sorted media items */} + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/routes/caregiver/CaregiverLogin.tsx b/src/routes/caregiver/CaregiverLogin.tsx index eefb4e6..707ec8a 100644 --- a/src/routes/caregiver/CaregiverLogin.tsx +++ b/src/routes/caregiver/CaregiverLogin.tsx @@ -54,7 +54,10 @@ export default function CaregiverLogin() { // User found if (user.docs.length > 0) { console.log("Signed in as: ", user.docs[0].data().lastName); + localStorage.setItem("lastName", userLastName); + localStorage.setItem("passcode", userPasscode); navigate("/"); + } // User not found From 594a5c8aa4f0a20eaa4fc2ea0b4b99c05da71b12 Mon Sep 17 00:00:00 2001 From: Khanh Bui Date: Sat, 26 Oct 2024 21:22:53 -0600 Subject: [PATCH 2/2] fixed dropdown ui, and change dropdown default --- src/components/MediaGrid.tsx | 14 ++++++++------ src/components/SortDropdownList.tsx | 22 +++++++++++++++------- src/routes/MemberPage.tsx | 12 ++++++------ 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/components/MediaGrid.tsx b/src/components/MediaGrid.tsx index 0da1de9..2eb7c8f 100644 --- a/src/components/MediaGrid.tsx +++ b/src/components/MediaGrid.tsx @@ -10,16 +10,18 @@ interface Media { interface MediaGridProps { data: Media[]; - sortOrder: "latest" | "oldest"; // Sorting order as a prop + sortOrder: "latest" | "oldest" | null; // Sorting order as a prop } function MediaGrid({ data, sortOrder }: MediaGridProps) { // Sort data based on sortOrder - const sortedData = [...data].sort((a, b) => { - return sortOrder === "latest" - ? b.date.getTime() - a.date.getTime() // Descending for latest - : a.date.getTime() - b.date.getTime(); // Ascending for oldest - }); + const sortedData = sortOrder + ? [...data].sort((a, b) => { + return sortOrder === "latest" + ? b.date.getTime() - a.date.getTime() // Descending for latest + : a.date.getTime() - b.date.getTime(); // Ascending for oldest + }) + : data; // Use default order if sortOrder is null return (
diff --git a/src/components/SortDropdownList.tsx b/src/components/SortDropdownList.tsx index 531984a..8699004 100644 --- a/src/components/SortDropdownList.tsx +++ b/src/components/SortDropdownList.tsx @@ -2,14 +2,14 @@ import { CaretDown } from "@phosphor-icons/react"; import { useEffect, useRef, useState } from "react"; interface SortDropdownProps { - onSelect: (sortOrder: string) => void; // Callback prop for selecting sort order + onSelect: (sortOrder: string | null) => void; // null for default sorting } function SortDropdown({ onSelect }: SortDropdownProps) { // State to manage the dropdown open/closed state const [isDropdownOpen, setIsDropdownOpen] = useState(false); // State to keep track of the current sort order - const [sortOrder, setSortOrder] = useState("latest"); + const [sortOrder, setSortOrder] = useState(null); // Default to null // Ref to handle clicks outside the dropdown const dropdownRef = useRef(null); @@ -30,7 +30,7 @@ function SortDropdown({ onSelect }: SortDropdownProps) { }, []); // Function to handle selection of sort order - const handleSortSelect = (order: string) => { + const handleSortSelect = (order: string | null) => { setSortOrder(order); // Update the sort order onSelect(order); // Call the passed in prop to update sort order setIsDropdownOpen(false); // Close the dropdown after selection @@ -49,18 +49,26 @@ function SortDropdown({ onSelect }: SortDropdownProps) { {/* Dropdown Menu */} {isDropdownOpen && ( -
+
handleSortSelect("latest")} // Sort by latest > - Latest + Newest to Oldest
handleSortSelect("oldest")} // Sort by oldest > - Oldest + Oldest to Newest +
+
+
handleSortSelect(null)} // Reset to no sorting + > + Reset +
)} diff --git a/src/routes/MemberPage.tsx b/src/routes/MemberPage.tsx index 5218ece..6e0a125 100644 --- a/src/routes/MemberPage.tsx +++ b/src/routes/MemberPage.tsx @@ -119,7 +119,7 @@ const usersRef = collection(database, "users"); export default function MemberPage() { const navigate = useNavigate(); - const [sortOrder, setSortOrder] = useState<'latest' | 'oldest'>('latest'); + const [sortOrder, setSortOrder] = useState<"latest" | "oldest" | null>(null); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [userData, setUserData] = useState(null); @@ -155,11 +155,11 @@ export default function MemberPage() { fetchData(); }, [navigate]); - // Function to update sortOrder and close the dropdown menu - const handleSortSelect = (order:SortOrder) => { - setSortOrder(order); // Update sort order (latest or oldest) - setIsDropdownOpen(false); // Close dropdown after selection - }; + // Function to update sortOrder and close the dropdown menu + const handleSortSelect = (order: "latest" | "oldest" | null) => { + setSortOrder(order); // Update sort order (latest or oldest) + setIsDropdownOpen(false); // Close dropdown after selection + }; return (