From 596f648f31682e9328e61669647406f297675b45 Mon Sep 17 00:00:00 2001 From: Andy Chan Date: Wed, 20 Nov 2024 15:54:54 -0500 Subject: [PATCH 01/14] Redesign landing page (#44) * Redesign landing page A bit empty right now, functionality non-existent * Create search bar and filter pills Not tied to any functionality at the moment, however * Implement search result component Searching not yet added, sample results added for testing * Move ref in FilterPill to allow closing by clicking pill Clicking on the pill would reopen the dropdown; it closes properly now --- compass/app/home/page.tsx | 56 +--- .../components/resource/LandingSearchBar.tsx | 282 +++++++++++++++--- compass/components/resource/SearchResult.tsx | 44 +++ .../components/resource/sample_results.json | 32 ++ 4 files changed, 328 insertions(+), 86 deletions(-) create mode 100644 compass/components/resource/SearchResult.tsx create mode 100644 compass/components/resource/sample_results.json diff --git a/compass/app/home/page.tsx b/compass/app/home/page.tsx index 86be3ef..c656fd4 100644 --- a/compass/app/home/page.tsx +++ b/compass/app/home/page.tsx @@ -2,60 +2,20 @@ import Callout from "@/components/resource/Callout"; import Card from "@/components/resource/Card"; import { LandingSearchBar } from "@/components/resource/LandingSearchBar"; -import { - BookOpenIcon, - BookmarkIcon, - ClipboardIcon, -} from "@heroicons/react/24/solid"; +import { SearchResult } from "@/components/resource/SearchResult"; import Image from "next/image"; import Link from "next/link"; export default function Page() { return ( -
- {/* icon + title */} -
-
- Compass Center logo. -

- Compass Center Advocate Landing Page -

-
- - Welcome! Below you will find a list of resources for the - Compass Center's trained advocates. These materials - serve to virtually provide a collection of advocacy, - resource, and hotline manuals and information. - - {" "} - If you are an advocate looking for the contact - information of a particular Compass Center employee, - please directly contact your staff back-up or the person - in charge of your training. - - -
-
- {/* link to different pages */} -
- - } text="Resources" /> - - - } text="Services" /> - - - } text="Training Manuals" /> - -
- {/* search bar */} - +
+
+

+ Good evening! +

+ +
); } diff --git a/compass/components/resource/LandingSearchBar.tsx b/compass/components/resource/LandingSearchBar.tsx index afce772..399a71d 100644 --- a/compass/components/resource/LandingSearchBar.tsx +++ b/compass/components/resource/LandingSearchBar.tsx @@ -1,17 +1,41 @@ +import { FunnelIcon as FunnelIconOutline } from "@heroicons/react/24/outline"; import { + ArchiveBoxIcon, ChevronDownIcon, + FunnelIcon, MagnifyingGlassIcon, + TagIcon, XMarkIcon, } from "@heroicons/react/24/solid"; -import React, { useState } from "react"; +import React, { + ReactNode, + SetStateAction, + useState, + useRef, + useEffect, +} from "react"; import Image from "next/image"; -import { FilterBox } from "../FilterBox"; +import { SearchResult } from "./SearchResult"; + +// TODO: Actually implement search. +import sampleResults from "./sample_results.json"; export const LandingSearchBar: React.FC = () => { const [searchTerm, setSearchTerm] = useState(""); + const [showFilters, setShowFilters] = useState(false); const [showFilterBox, setShowFilterBox] = useState(false); const toggleFilterBox = () => setShowFilterBox((prev) => !prev); + const collections = ["Resources", "Services"]; + const [selectedCollections, setSelectedCollections] = useState( + new Array(collections.length).fill(false) + ); + + const tags = ["Food Relief", "Period Poverty", "Nutrition Education"]; + const [selectedTags, setSelectedTags] = useState( + new Array(tags.length).fill(false) + ); + const handleSearchChange = (event: React.ChangeEvent) => { setSearchTerm(event.target.value); }; @@ -21,53 +45,235 @@ export const LandingSearchBar: React.FC = () => { }; return ( -
+
{/* searchbar */} -
-
- -
- {/* input */} - {searchTerm && ( - + )} + {/* Filter button */} + - )} -
- - - - {showFilterBox && } -
+ + {/* Search filters */} +
+ + +
+ {/* search results, for now since it's empty this is the default screen */} -
+
0 ? " hidden" : "") + } + > Landing illustration -

- Need to find something? Use the links or the search bar - above to get your results. -

+

+ Need to find something? Use the search bar above to get your + results. +

+
+ +
0 ? "" : " hidden") + } + > + {sampleResults.map((result, i) => ( + + ))} +
+
+ ); +}; + +// Closes the filter dropdown when the user clicks outside the dropdown. +const useFilterPillDropdown = ( + ref: React.RefObject, + setShowDropdown: Function +) => { + // Close on outside click + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + if (setShowDropdown) setShowDropdown(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => + // Unbind the event listener on cleanup. + document.removeEventListener("mousedown", handleClickOutside); + }, [ref, setShowDropdown]); +}; + +// Props for the filter pill... +interface FilterPillProps { + icon: React.ForwardRefExoticComponent< + Omit, "ref"> + >; + name: string; + searchBar?: boolean; + options: string[]; + selectedOptions: boolean[]; + setSelectedOptions: React.Dispatch>; +} + +// The filter pill (visible when filter button active, contains dropdown) +const FilterPill: React.FC = ({ + icon, + name, + options, + selectedOptions, + setSelectedOptions, + searchBar = false, +}) => { + const Icon = icon; + const [showDropdown, setShowDropdown] = useState(false); + const [isActive, setIsActive] = useState(false); + const dropdownRef = useRef(null); + + const handleCheck = ( + e: React.ChangeEvent, + item: string + ) => { + const selected = selectedOptions.map((o, i) => { + if (i == options.indexOf(item)) { + return e.target.checked; + } else { + return o; + } + }); + + setSelectedOptions(selected); + setIsActive(selected.includes(true)); + }; + + // Closes dropdown when clicked outside + useFilterPillDropdown(dropdownRef, setShowDropdown); + + return ( +
+ {/* The filter pill */} + + + {/* The filter option selection dropdown */} +
+ + {options.map((item, index) => { + return ( + + ); + })}
); diff --git a/compass/components/resource/SearchResult.tsx b/compass/components/resource/SearchResult.tsx new file mode 100644 index 0000000..42b1b9e --- /dev/null +++ b/compass/components/resource/SearchResult.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { + BookmarkIcon, + ClipboardIcon, + QuestionMarkCircleIcon, + ArrowUturnRightIcon, +} from "@heroicons/react/24/solid"; + +interface SearchResultProps { + type: "resource" | "service" | string; + name: string; + description: string; +} + +export const SearchResult: React.FC = ({ + type, + name, + description, +}) => { + const Icon: React.ForwardRefExoticComponent< + Omit, "ref"> + > = + type === "resource" + ? BookmarkIcon + : type === "service" + ? ClipboardIcon + : QuestionMarkCircleIcon; // Unknown type + + return ( +
+ {/* Left side of the item */} +
+ + + {name} + + + {description} + +
+ +
+ ); +}; diff --git a/compass/components/resource/sample_results.json b/compass/components/resource/sample_results.json new file mode 100644 index 0000000..275250c --- /dev/null +++ b/compass/components/resource/sample_results.json @@ -0,0 +1,32 @@ +[ + { + "type": "resource", + "name": "example name", + "description": "example description" + }, + { + "type": "service", + "name": "example name", + "description": "example description" + }, + { + "type": "resource", + "name": "National Domestic Violence Hotline", + "description": "24/7 confidential support for victims of domestic violence" + }, + { + "type": "resource", + "name": "Legal Aid Society", + "description": "Free legal assistance for low-income individuals" + }, + { + "type": "service", + "name": "Crisis Hotline", + "description": "24/7 support for individuals in crisis" + }, + { + "type": "unknown", + "name": "unknown thing with a really long name", + "description": "and let's also type out a really long description to see how it handles overflow and all that anyways" + } +] From fdbf4ffa40e5a6f5770ff231a3d7f8f1f8fa0d6b Mon Sep 17 00:00:00 2001 From: Prajwal Moharana <78167757+pmoharana-cmd@users.noreply.github.com> Date: Sun, 15 Dec 2024 22:48:15 -0500 Subject: [PATCH 02/14] Frontend loading indicator foster (#47) * initial layout component but in sidebar only * loading for sign out * Add loading functionality for changing pages --------- Co-authored-by: emmalynf --- compass/README.md | 4 +- compass/app/home/layout.tsx | 3 + compass/components/RootLayout.tsx | 67 +++++++++++++++++++ .../components/Sidebar/LoadingIcon.module.css | 19 ++++++ compass/components/Sidebar/LoadingIcon.tsx | 14 ++++ compass/components/Sidebar/Sidebar.tsx | 29 ++++++-- compass/components/Sidebar/SidebarItem.tsx | 3 + compass/components/auth/LoggingOut.tsx | 22 ++++++ compass/components/resource/UserProfile.tsx | 12 +++- 9 files changed, 164 insertions(+), 9 deletions(-) create mode 100644 compass/components/RootLayout.tsx create mode 100644 compass/components/Sidebar/LoadingIcon.module.css create mode 100644 compass/components/Sidebar/LoadingIcon.tsx create mode 100644 compass/components/auth/LoggingOut.tsx diff --git a/compass/README.md b/compass/README.md index 5ce4a7c..c403366 100644 --- a/compass/README.md +++ b/compass/README.md @@ -24,8 +24,8 @@ This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-opti To learn more about Next.js, take a look at the following resources: -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! diff --git a/compass/app/home/layout.tsx b/compass/app/home/layout.tsx index 236616e..b98493c 100644 --- a/compass/app/home/layout.tsx +++ b/compass/app/home/layout.tsx @@ -15,6 +15,7 @@ export default function RootLayout({ const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [user, setUser] = useState(); const router = useRouter(); + const [loading, setLoading] = useState(true); useEffect(() => { async function getUser() { @@ -35,6 +36,7 @@ export default function RootLayout({ ); setUser(await userData.json()); + setLoading(false); } getUser(); @@ -50,6 +52,7 @@ export default function RootLayout({ setIsSidebarOpen={setIsSidebarOpen} isSidebarOpen={isSidebarOpen} isAdmin={user.role === Role.ADMIN} + loading={loading} />
(null); // Initialize user as null + const router = useRouter(); + + useEffect(() => { + async function getUser() { + const supabase = createClient(); + + const { data, error } = await supabase.auth.getUser(); + + if (error || !data?.user) { + console.log("User not logged in or error fetching user"); + router.push("/auth/login"); + return; + } + + const userData = await fetch( + `${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}` + ); + + const user: User = await userData.json(); + setUser(user); // Set user data after fetching + } + + getUser(); + }, [router]); + + if (!user) { + return ; // Show loading screen while the user is being fetched + } + + return ( +
+ {/* Sidebar is shared across all pages */} + + + {/* Page content */} +
+ {children} {/* Render page-specific content here */} +
+
+ ); +} diff --git a/compass/components/Sidebar/LoadingIcon.module.css b/compass/components/Sidebar/LoadingIcon.module.css new file mode 100644 index 0000000..7c2947e --- /dev/null +++ b/compass/components/Sidebar/LoadingIcon.module.css @@ -0,0 +1,19 @@ +/* components/LoadingIcon.module.css */ +.loader { + width: 24px; /* Larger for better visibility */ + height: 24px; + border: 4px solid #5b21b6; /* Primary color */ + border-top: 4px solid #ffffff; /* Contrasting color */ + border-radius: 50%; + animation: spin 1s linear infinite; /* Smooth continuous spin */ + margin-bottom: 20px; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); /* Start position */ + } + 100% { + transform: rotate(360deg); /* Full rotation */ + } +} diff --git a/compass/components/Sidebar/LoadingIcon.tsx b/compass/components/Sidebar/LoadingIcon.tsx new file mode 100644 index 0000000..508d4dc --- /dev/null +++ b/compass/components/Sidebar/LoadingIcon.tsx @@ -0,0 +1,14 @@ +// components/Loading.js +import styles from "./LoadingIcon.module.css"; + +const LoadingIcon = () => { + return ( +
+
+
+
+
+ ); +}; + +export default LoadingIcon; diff --git a/compass/components/Sidebar/Sidebar.tsx b/compass/components/Sidebar/Sidebar.tsx index d82ab69..7ab2135 100644 --- a/compass/components/Sidebar/Sidebar.tsx +++ b/compass/components/Sidebar/Sidebar.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { HomeIcon, ChevronDoubleLeftIcon, @@ -9,7 +9,9 @@ import { LockClosedIcon, } from "@heroicons/react/24/solid"; import { SidebarItem } from "./SidebarItem"; +import styles from "./LoadingIcon.module.css"; import { UserProfile } from "../resource/UserProfile"; +import LoadingIcon from "./LoadingIcon"; interface SidebarProps { setIsSidebarOpen: React.Dispatch>; @@ -17,6 +19,7 @@ interface SidebarProps { name: string; email: string; isAdmin: boolean; + loading: boolean; } const Sidebar: React.FC = ({ @@ -25,7 +28,9 @@ const Sidebar: React.FC = ({ name, email, isAdmin: admin, + loading, }) => { + const [isLoading, setIsLoading] = useState(false); return ( <> {/* Button to open the sidebar. */} @@ -62,11 +67,22 @@ const Sidebar: React.FC = ({
+ {/* Loading indicator*/} + {isLoading && ( +
+ +
+ )} +
- {/* user + logout button */} -
- +
+
+ {/* navigation menu */}

@@ -79,6 +95,7 @@ const Sidebar: React.FC = ({ text="Admin" active={true} redirect="/admin" + onClick={setIsLoading} /> )} @@ -87,24 +104,28 @@ const Sidebar: React.FC = ({ text="Home" active={true} redirect="/home" + onClick={setIsLoading} /> } text="Resources" active={true} redirect="/resource" + onClick={setIsLoading} /> } text="Services" active={true} redirect="/service" + onClick={setIsLoading} /> } text="Training Manuals" active={true} redirect="/training-manuals" + onClick={setIsLoading} />

diff --git a/compass/components/Sidebar/SidebarItem.tsx b/compass/components/Sidebar/SidebarItem.tsx index 34d3541..475f7b5 100644 --- a/compass/components/Sidebar/SidebarItem.tsx +++ b/compass/components/Sidebar/SidebarItem.tsx @@ -5,6 +5,7 @@ interface SidebarItemProps { text: string; active: boolean; redirect: string; + onClick: React.Dispatch>; } export const SidebarItem: React.FC = ({ @@ -12,9 +13,11 @@ export const SidebarItem: React.FC = ({ text, active, redirect, + onClick, }) => { return ( onClick(true)} href={redirect} className={ active diff --git a/compass/components/auth/LoggingOut.tsx b/compass/components/auth/LoggingOut.tsx new file mode 100644 index 0000000..8915e23 --- /dev/null +++ b/compass/components/auth/LoggingOut.tsx @@ -0,0 +1,22 @@ +// components/LoggingOut.js +import styles from "./Loading.module.css"; +import Image from "next/image"; + +const LoggingOut = () => { + return ( +
+
+ Compass Center logo. +

Signing out...

+
+
+
+ ); +}; + +export default LoggingOut; diff --git a/compass/components/resource/UserProfile.tsx b/compass/components/resource/UserProfile.tsx index d2780a6..6d3c291 100644 --- a/compass/components/resource/UserProfile.tsx +++ b/compass/components/resource/UserProfile.tsx @@ -1,15 +1,21 @@ +import { useState } from "react"; import { signOut } from "@/app/auth/actions"; interface UserProfileProps { name: string; email: string; + setLoading: React.Dispatch>; } -const handleClick = async (event: React.MouseEvent) => { +const handleClick = async ( + event: React.MouseEvent, + setLoading: React.Dispatch> +) => { + setLoading(true); await signOut(); }; -export const UserProfile = ({ name, email }: UserProfileProps) => { +export const UserProfile = ({ name, email, setLoading }: UserProfileProps) => { return (
@@ -19,7 +25,7 @@ export const UserProfile = ({ name, email }: UserProfileProps) => { {email}
-
@@ -145,7 +111,7 @@ const Drawer: FunctionComponent = ({

- {rowContent.username} + {rowContent[titleKey]}

@@ -170,81 +136,116 @@ const Drawer: FunctionComponent = ({
- - - -
-
- - - - - -
-
- - - - - -
-
- - - - - -
-
- +
+ {details.map((detail, index) => { + const value = tempRowContent[detail.key]; + let valueToRender = <>; + + switch (detail.inputType) { + case "select-one": + case "select-multiple": + valueToRender = ( +
+
+ {}) + } + /> +
+
+ ); + break; + case "textarea": + valueToRender = ( +
+
+
- - Username - -
- - Role - -
- - Email - -
- - Type of Program