diff --git a/src/comps/admin/AdminNav.tsx b/src/comps/admin/AdminNav.tsx index 26fac32..32848d2 100644 --- a/src/comps/admin/AdminNav.tsx +++ b/src/comps/admin/AdminNav.tsx @@ -1,51 +1,57 @@ -import RouteTabs from "../ui/RouteTabs"; -import PendingActionsIcon from "@mui/icons-material/PendingActions"; -import EditIcon from "@mui/icons-material/Edit"; -import ReportProblemIcon from "@mui/icons-material/ReportProblem"; -import EmailIcon from "@mui/icons-material/Email"; -import CampaignIcon from "@mui/icons-material/Campaign"; -import MeetingRoomIcon from "@mui/icons-material/MeetingRoom"; -import { Box } from "@mui/material"; - -const AdminNav = () => { - let navLinks = [ - { - to: "/admin/approve-pending", - label: "Pending Orgs", - icon: , - }, - { - to: "/admin/approve-edit", - label: "Approve Edits", - icon: , - }, - { - to: "/admin/strikes", - label: "Strikes", - icon: , - }, - { - to: "/admin/send-message", - label: "Send Message", - icon: , - }, - { - to: "/admin/announcements", - label: "Announcements", - icon: , - }, - { - to: "/admin/rooms", - label: "Rooms", - icon: , - }, - ]; - - return ( - - - - ); -}; - -export default AdminNav; +import RouteTabs from "../ui/RouteTabs"; +import PendingActionsIcon from "@mui/icons-material/PendingActions"; +import EditIcon from "@mui/icons-material/Edit"; +import ReportProblemIcon from "@mui/icons-material/ReportProblem"; +import EmailIcon from "@mui/icons-material/Email"; +import CampaignIcon from "@mui/icons-material/Campaign"; +import MeetingRoomIcon from "@mui/icons-material/MeetingRoom"; +import { Box } from "@mui/material"; +import { Send } from "@mui/icons-material"; + +const AdminNav = () => { + let navLinks = [ + { + to: "/admin/approve-pending", + label: "Pending Orgs", + icon: , + }, + { + to: "/admin/approve-edit", + label: "Approve Edits", + icon: , + }, + { + to: "/admin/strikes", + label: "Strikes", + icon: , + }, + { + to: "/admin/send-message", + label: "Send Message", + icon: , + }, + { + to: "/admin/announcements", + label: "Announcements", + icon: , + }, + { + to: "/admin/rooms", + label: "Rooms", + icon: , + }, + { + to: "/admin/club-admin-emails", + label: "Admin Emails", + icon: , + }, + ]; + + return ( + + + + ); +}; + +export default AdminNav; diff --git a/src/comps/pages/orgs/OrgNav.tsx b/src/comps/pages/orgs/OrgNav.tsx index eb28338..8fab31c 100644 --- a/src/comps/pages/orgs/OrgNav.tsx +++ b/src/comps/pages/orgs/OrgNav.tsx @@ -1,324 +1,326 @@ -import { useContext, useEffect, useMemo, useState } from "react"; - -import { - Box, - Typography, - Divider, - List, - ListItemButton, - ListItemText, - ListItemIcon, - Link, - Avatar, -} from "@mui/material"; - -import OrgContext from "../../context/OrgContext"; -import UserContext from "../../context/UserContext"; - -import { supabase } from "../../../supabaseClient"; -import { useSnackbar } from "notistack"; - -import { Link as NavLink, useLocation, useNavigate } from "react-router-dom"; -import InfoIcon from "@mui/icons-material/Info"; -import ArticleIcon from "@mui/icons-material/Article"; -import CalendarMonthIcon from "@mui/icons-material/CalendarMonth"; -import PeopleIcon from "@mui/icons-material/People"; -import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings"; -import AsyncButton from "../../ui/AsyncButton"; - -const OrgNav = ({ isMobile }: { isMobile: boolean }) => { - const organization = useContext(OrgContext); - const user = useContext(UserContext); - const { enqueueSnackbar } = useSnackbar(); - const navigate = useNavigate(); - const main = `/${organization.url}`; - - const location = useLocation(); - - const navLinks = [ - { to: main, display: "Overview", icon: }, - { to: `${main}/charter`, display: "Charter", icon: }, - { - to: `${main}/meetings`, - display: "Meetings", - icon: , - }, - { to: `${main}/members`, display: "Members", icon: }, - ]; - - const [currentIndex, setCurrentIndex] = useState(0); - const [attemptingInteract, setAttemptingInteract] = useState(false); // if in middle of sending join request, lock the button - - useEffect(() => { - let isCorrectIndex = false; - if (location.pathname === main) { - if (currentIndex !== 0) { - setCurrentIndex(0); - } else { - isCorrectIndex = true; - } - } else { - if (currentIndex === 0) { - isCorrectIndex = location.pathname === main; - } else { - isCorrectIndex = location.pathname.startsWith( - navLinks[currentIndex]?.to, - ); - } - } - - if (!isCorrectIndex) { - const correctIndex = - navLinks - .slice(1) - .findIndex((link) => - location.pathname.startsWith(link.to), - ) + 1; - setCurrentIndex(~correctIndex ? correctIndex : 0); - } - }, [navLinks, location.pathname, currentIndex, main]); - - const isInOrg: boolean = !!organization.memberships?.find( - (m) => m.users?.id === user.id, - ); - let isCreator = false; - let isAdmin = false; - let isActive = false; - - /* CHECK IF CREATOR */ - if ( - isInOrg && - organization.memberships?.find((m) => m.users?.id === user.id)?.role === - "CREATOR" - ) { - isCreator = true; - isAdmin = true; - } - - /* CHECK IF ADMIN */ - if ( - isInOrg && - organization.memberships?.find((m) => m.users?.id === user.id)?.role === - "ADMIN" - ) { - isAdmin = true; - } - - /* CHECK IF MEMBERSHIP IS ACTIVE */ - if ( - isInOrg && - organization.memberships?.find((m) => m.users?.id === user.id)?.active - ) { - isActive = true; - } - - /* Button on OrgNav that changes depending on the user */ - let interactString = isInOrg - ? isActive - ? "LEAVE" - : "CANCEL JOIN" - : "JOIN"; - const handleInteract = async () => { - setAttemptingInteract(true); - if (interactString === "JOIN") { - /* JOIN ORGANIZATION */ - let body = { - organization_id: organization.id, - }; - const { data, error } = await supabase.functions.invoke( - "join-organization", - { body }, - ); - if (error || !data) { - return enqueueSnackbar( - "Unable to join organization. Contact it@stuysu.org for support.", - { variant: "error" }, - ); - } - - // update context - if (organization.setOrg) { - organization.setOrg({ - ...organization, - memberships: [...organization.memberships, data], - }); - } - - enqueueSnackbar("Sent organization a join request!", { - variant: "success", - }); - } else if ( - interactString === "LEAVE" || - interactString === "CANCEL JOIN" - ) { - /* LEAVE ORGANIZATION */ - let membership = organization.memberships?.find( - (m) => m.users?.id === user.id, - ); - - const { error } = await supabase - .from("memberships") - .delete() - .eq("id", membership?.id); - - if (error) { - return enqueueSnackbar( - "Unable to leave organization. Contact it@stuysu.org for support.", - { variant: "error" }, - ); - } - - // update context - if (organization.setOrg) { - organization.setOrg({ - ...organization, - memberships: organization.memberships.filter( - (m) => m.users?.id !== user.id, - ), - }); - } - - enqueueSnackbar("Left organization!", { variant: "success" }); - } - setAttemptingInteract(false); - }; - - let disabled = false; - if (isCreator) disabled = true; - if (!isInOrg && !organization.joinable) { - interactString = "JOINING DISABLED"; - disabled = true; - } else if (!user.signed_in) { - interactString = "sign in to join"; - disabled = true; - } - - if (isAdmin) { - navLinks.push({ - to: `${main}/admin`, - display: "Admin", - icon: , - }); - } - - return ( - - - - - {organization.name.charAt(0).toUpperCase()} - - - - {organization.name} - - - {organization.purpose} - - {organization.socials && - organization.socials.split(" ").map((social, i, a) => { - if (!social.startsWith("http")) { - let outText = social; - - if (i !== a.length - 1) { - outText += " "; - } - - return outText; - } - - return ( - - {social} - - ); - })} - - - {interactString} - - - - - - - - {navLinks.map((linkData, i) => ( - { - navigate(linkData.to); - setCurrentIndex(i); - }} - > - {linkData.icon && ( - {linkData.icon} - )} - {linkData.display} - - ))} - - - ); -}; - -export default OrgNav; +import { useContext, useEffect, useMemo, useState } from "react"; + +import { + Box, + Typography, + Divider, + List, + ListItemButton, + ListItemText, + ListItemIcon, + Link, + Avatar, +} from "@mui/material"; + +import OrgContext from "../../context/OrgContext"; +import UserContext from "../../context/UserContext"; + +import { supabase } from "../../../supabaseClient"; +import { useSnackbar } from "notistack"; + +import { Link as NavLink, useLocation, useNavigate } from "react-router-dom"; +import InfoIcon from "@mui/icons-material/Info"; +import ArticleIcon from "@mui/icons-material/Article"; +import CalendarMonthIcon from "@mui/icons-material/CalendarMonth"; +import PeopleIcon from "@mui/icons-material/People"; +import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings"; +import AsyncButton from "../../ui/AsyncButton"; +import { PostAdd } from "@mui/icons-material"; + +const OrgNav = ({ isMobile }: { isMobile: boolean }) => { + const organization = useContext(OrgContext); + const user = useContext(UserContext); + const { enqueueSnackbar } = useSnackbar(); + const navigate = useNavigate(); + const main = `/${organization.url}`; + + const location = useLocation(); + + const navLinks = [ + { to: main, display: "Overview", icon: }, + { to: `${main}/charter`, display: "Charter", icon: }, + { + to: `${main}/meetings`, + display: "Meetings", + icon: , + }, + { to: `${main}/members`, display: "Members", icon: }, + { to: `${main}/posts`, display: "Posts", icon: }, + ]; + + const [currentIndex, setCurrentIndex] = useState(0); + const [attemptingInteract, setAttemptingInteract] = useState(false); // if in middle of sending join request, lock the button + + useEffect(() => { + let isCorrectIndex = false; + if (location.pathname === main) { + if (currentIndex !== 0) { + setCurrentIndex(0); + } else { + isCorrectIndex = true; + } + } else { + if (currentIndex === 0) { + isCorrectIndex = location.pathname === main; + } else { + isCorrectIndex = location.pathname.startsWith( + navLinks[currentIndex]?.to, + ); + } + } + + if (!isCorrectIndex) { + const correctIndex = + navLinks + .slice(1) + .findIndex((link) => + location.pathname.startsWith(link.to), + ) + 1; + setCurrentIndex(~correctIndex ? correctIndex : 0); + } + }, [navLinks, location.pathname, currentIndex, main]); + + const isInOrg: boolean = !!organization.memberships?.find( + (m) => m.users?.id === user.id, + ); + let isCreator = false; + let isAdmin = false; + let isActive = false; + + /* CHECK IF CREATOR */ + if ( + isInOrg && + organization.memberships?.find((m) => m.users?.id === user.id)?.role === + "CREATOR" + ) { + isCreator = true; + isAdmin = true; + } + + /* CHECK IF ADMIN */ + if ( + isInOrg && + organization.memberships?.find((m) => m.users?.id === user.id)?.role === + "ADMIN" + ) { + isAdmin = true; + } + + /* CHECK IF MEMBERSHIP IS ACTIVE */ + if ( + isInOrg && + organization.memberships?.find((m) => m.users?.id === user.id)?.active + ) { + isActive = true; + } + + /* Button on OrgNav that changes depending on the user */ + let interactString = isInOrg + ? isActive + ? "LEAVE" + : "CANCEL JOIN" + : "JOIN"; + const handleInteract = async () => { + setAttemptingInteract(true); + if (interactString === "JOIN") { + /* JOIN ORGANIZATION */ + let body = { + organization_id: organization.id, + }; + const { data, error } = await supabase.functions.invoke( + "join-organization", + { body }, + ); + if (error || !data) { + return enqueueSnackbar( + "Unable to join organization. Contact it@stuysu.org for support.", + { variant: "error" }, + ); + } + + // update context + if (organization.setOrg) { + organization.setOrg({ + ...organization, + memberships: [...organization.memberships, data], + }); + } + + enqueueSnackbar("Sent organization a join request!", { + variant: "success", + }); + } else if ( + interactString === "LEAVE" || + interactString === "CANCEL JOIN" + ) { + /* LEAVE ORGANIZATION */ + let membership = organization.memberships?.find( + (m) => m.users?.id === user.id, + ); + + const { error } = await supabase + .from("memberships") + .delete() + .eq("id", membership?.id); + + if (error) { + return enqueueSnackbar( + "Unable to leave organization. Contact it@stuysu.org for support.", + { variant: "error" }, + ); + } + + // update context + if (organization.setOrg) { + organization.setOrg({ + ...organization, + memberships: organization.memberships.filter( + (m) => m.users?.id !== user.id, + ), + }); + } + + enqueueSnackbar("Left organization!", { variant: "success" }); + } + setAttemptingInteract(false); + }; + + let disabled = false; + if (isCreator) disabled = true; + if (!isInOrg && !organization.joinable) { + interactString = "JOINING DISABLED"; + disabled = true; + } else if (!user.signed_in) { + interactString = "sign in to join"; + disabled = true; + } + + if (isAdmin) { + navLinks.push({ + to: `${main}/admin`, + display: "Admin", + icon: , + }); + } + + return ( + + + + + {organization.name.charAt(0).toUpperCase()} + + + + {organization.name} + + + {organization.purpose} + + {organization.socials && + organization.socials.split(" ").map((social, i, a) => { + if (!social.startsWith("http")) { + let outText = social; + + if (i !== a.length - 1) { + outText += " "; + } + + return outText; + } + + return ( + + {social} + + ); + })} + + + {interactString} + + + + + + + + {navLinks.map((linkData, i) => ( + { + navigate(linkData.to); + setCurrentIndex(i); + }} + > + {linkData.icon && ( + {linkData.icon} + )} + {linkData.display} + + ))} + + + ); +}; + +export default OrgNav; diff --git a/src/comps/pages/orgs/admin/PendingMember.tsx b/src/comps/pages/orgs/admin/PendingMember.tsx index c8ed4ca..f056cb3 100644 --- a/src/comps/pages/orgs/admin/PendingMember.tsx +++ b/src/comps/pages/orgs/admin/PendingMember.tsx @@ -1,123 +1,143 @@ -import { supabase } from "../../../../supabaseClient"; -import { Box } from "@mui/material"; -import { useSnackbar } from "notistack"; -import { useContext } from "react"; -import OrgContext from "../../../context/OrgContext"; -import OrgMember from "../OrgMember"; -import AsyncButton from "../../../ui/AsyncButton"; - -const PendingMember = ({ - id, - first_name, - last_name, - email, - picture, -}: { - id: number; - first_name?: string; - last_name?: string; - email: string; - picture: string | undefined; -}) => { - const { enqueueSnackbar } = useSnackbar(); - const organization = useContext(OrgContext); - - const handleApprove = async () => { - const { error } = await supabase.functions.invoke("approve-member", { - body: { member_id: id }, - }); - - if (error) { - enqueueSnackbar( - "Error approving member. Contact it@stuysu.org for support.", - { variant: "error" }, - ); - return; - } - - let memberIndex = organization.memberships.findIndex( - (m) => m.id === id, - ); - let memberData = organization.memberships[memberIndex]; - - memberData.active = true; - - // update context - if (organization.setOrg) { - organization.setOrg({ - ...organization, - memberships: [ - ...organization.memberships.slice(0, memberIndex), - memberData, - ...organization.memberships.slice(memberIndex + 1), - ], - }); - } - - enqueueSnackbar("Member approved!", { variant: "success" }); - }; - - const handleReject = async () => { - const { error } = await supabase - .from("memberships") - .delete() - .eq("id", id); - if (error) { - enqueueSnackbar( - "Error rejecting member. Contact it@stuysu.org for support.", - { variant: "error" }, - ); - return; - } - - // update context - if (organization.setOrg) { - organization.setOrg({ - ...organization, - memberships: organization.memberships.filter( - (m) => m.id !== id, - ), - }); - } - - enqueueSnackbar("User rejected!", { variant: "success" }); - }; - - return ( - - - - - - - Approve - - - Reject - - - - ); -}; - -export default PendingMember; +import { supabase } from "../../../../supabaseClient"; +import { Box } from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useContext, useEffect } from "react"; +import OrgContext from "../../../context/OrgContext"; +import OrgMember from "../OrgMember"; +import AsyncButton from "../../../ui/AsyncButton"; + +const PendingMember = ({ + id, + first_name, + last_name, + email, + picture, + auto, +}: { + id: number; + first_name?: string; + last_name?: string; + email: string; + picture: string | undefined; + auto: boolean; +}) => { + const { enqueueSnackbar } = useSnackbar(); + const organization = useContext(OrgContext); + + const handleApprove = async () => { + const { error } = await supabase.functions.invoke("approve-member", { + body: { member_id: id }, + }); + + if (error) { + enqueueSnackbar( + "Error approving member. Contact it@stuysu.org for support.", + { variant: "error" }, + ); + return; + } + + let memberIndex = organization.memberships.findIndex( + (m) => m.id === id, + ); + let memberData = organization.memberships[memberIndex]; + + memberData.active = true; + + // update context + if (organization.setOrg) { + organization.setOrg({ + ...organization, + memberships: [ + ...organization.memberships.slice(0, memberIndex), + memberData, + ...organization.memberships.slice(memberIndex + 1), + ], + }); + } + + enqueueSnackbar("Member approved!", { variant: "success" }); + }; + + const handleReject = async () => { + const { error } = await supabase + .from("memberships") + .delete() + .eq("id", id); + if (error) { + enqueueSnackbar( + "Error rejecting member. Contact it@stuysu.org for support.", + { variant: "error" }, + ); + return; + } + + // update context + if (organization.setOrg) { + organization.setOrg({ + ...organization, + memberships: organization.memberships.filter( + (m) => m.id !== id, + ), + }); + } + + enqueueSnackbar("User rejected!", { variant: "success" }); + }; + + useEffect(() => { + if (auto) { + handleApprove(); + } + }, [auto]); + + return ( + + {!auto ? ( + + + + + + + Approve + + + Reject + + + + ) : ( + <> + )} + + ); +}; + +export default PendingMember; diff --git a/src/pages/admin/ClubAdminEmails.tsx b/src/pages/admin/ClubAdminEmails.tsx new file mode 100644 index 0000000..4502083 --- /dev/null +++ b/src/pages/admin/ClubAdminEmails.tsx @@ -0,0 +1,194 @@ +import { useSnackbar } from "notistack"; +import { useContext, useEffect, useState } from "react"; +import { supabase } from "../../supabaseClient"; +import { Box, TextField, Typography } from "@mui/material"; +import AsyncButton from "../../comps/ui/AsyncButton"; +import UserContext from "../../comps/context/UserContext"; + +const ClubAdminEmails = () => { + const { enqueueSnackbar } = useSnackbar(); + + const user = useContext(UserContext); + + const [orgId, setOrgId] = useState(); + const [orgName, setOrgName] = useState(""); + const [searchInput, setSearchInput] = useState(""); + const [filteredOrgs, setFilteredOrgs] = useState([]); + const [allOrgs, setAllOrgs] = useState([]); + const [currentOrganization, setCurrentOrganization] = + useState(); + const [adminEmails, setAdminEmails] = useState(""); + + useEffect(() => { + const fetchAllOrgs = async () => { + const { data, error } = await supabase + .from("organizations") + .select("*"); + if (error || !data) { + enqueueSnackbar( + "Failed to load organizations. Contact it@stuysu.org for support.", + { variant: "error" }, + ); + return; + } + setAllOrgs(data); + }; + + fetchAllOrgs(); + }, [enqueueSnackbar]); + + useEffect(() => { + if (searchInput) { + setFilteredOrgs( + allOrgs.filter((org) => + org.name.toLowerCase().includes(searchInput.toLowerCase()), + ), + ); + } else { + setFilteredOrgs([]); + } + }, [searchInput, allOrgs]); + + useEffect(() => { + const fetchAdminEmails = async () => { + const { data: memberships, error: membershipsError } = + await supabase + .from("clubadmins") + .select("email") + .eq("organization_id", currentOrganization?.id) + + if (membershipsError) { + enqueueSnackbar( + "Error fetching memberships. Please try again later.", + { variant: "error" }, + ); + return; + } + const adminEmails = memberships.map((user) => user.email).join(", "); + setAdminEmails(adminEmails); + }; + if (currentOrganization) { + fetchAdminEmails(); + } + }, [currentOrganization]); + + return ( + + + Club Admin Emails + + + setSearchInput(e.target.value)} + /> + + {filteredOrgs.length > 0 && ( + + {filteredOrgs.map((org) => ( + { + setOrgId(org.id); + setOrgName(org.name); + setCurrentOrganization(org); + setSearchInput(""); + setFilteredOrgs([]); + }} + sx={{ margin: "5px" }} + > + {org.name} + + ))} + + )} + {orgId && ( + <> + + + Org: {orgName} + + + + + + + { + try { + await navigator.clipboard.writeText( + adminEmails, + ); + enqueueSnackbar( + "Copied emails to clipboard!", + { + variant: "success", + }, + ); + } catch (error) { + enqueueSnackbar( + "Failed to copy emails to clipboard. :( Try manually copying from the page.", + { variant: "error" }, + ); + } + }} + variant="contained" + > + Copy + + + + + + )} + + ); +}; + +export default ClubAdminEmails; diff --git a/src/pages/admin/Strikes.tsx b/src/pages/admin/Strikes.tsx index a569415..87c7b83 100644 --- a/src/pages/admin/Strikes.tsx +++ b/src/pages/admin/Strikes.tsx @@ -1,259 +1,259 @@ -import { Box, TextField, Typography, Card } from "@mui/material"; -import { useState, useEffect } from "react"; -import { supabase } from "../../supabaseClient"; -import { useSnackbar } from "notistack"; -import AsyncButton from "../../comps/ui/AsyncButton"; - -const Strikes = () => { - const { enqueueSnackbar } = useSnackbar(); - - const [orgId, setOrgId] = useState(); - const [orgName, setOrgName] = useState(""); - const [orgStrikes, setOrgStrikes] = useState([]); - const [reason, setReason] = useState(""); - const [searchInput, setSearchInput] = useState(""); - const [filteredOrgs, setFilteredOrgs] = useState([]); - const [allOrgs, setAllOrgs] = useState([]); - - useEffect(() => { - const fetchAllOrgs = async () => { - const { data, error } = await supabase - .from("organizations") - .select("*"); - if (error || !data) { - enqueueSnackbar( - "Failed to load organizations. Contact it@stuysu.org for support.", - { variant: "error" }, - ); - return; - } - setAllOrgs(data); - }; - - fetchAllOrgs(); - }, [enqueueSnackbar]); - - useEffect(() => { - if (searchInput) { - setFilteredOrgs( - allOrgs.filter((org) => - org.name.toLowerCase().includes(searchInput.toLowerCase()), - ), - ); - } else { - setFilteredOrgs([]); - } - }, [searchInput, allOrgs]); - - useEffect(() => { - if (!orgId) return; - - const fetchOrgStrikes = async () => { - const { data, error } = await supabase - .from("strikes") - .select( - ` - id, - reason, - created_at, - organizations (name), - users (first_name, last_name, picture) - `, - ) - .eq("organization_id", orgId); - - if (error || !data) { - return enqueueSnackbar( - "Failed to load strikes. Contact it@stuysu.org for support.", - { variant: "error" }, - ); - } - - setOrgStrikes(data as Strike[]); - }; - - fetchOrgStrikes(); - }, [orgId, enqueueSnackbar]); - - const issueStrike = async () => { - const { data, error } = await supabase.functions.invoke( - "issue-strike", - { - body: { - organization_id: orgId, - reason, - }, - }, - ); - - if (error) { - enqueueSnackbar( - "Error issuing strike. Contact it@stuysu.org for support", - { variant: "error" }, - ); - return; - } - - setOrgStrikes([...orgStrikes, data as Strike]); - enqueueSnackbar("Strike issued!", { variant: "success" }); - setReason(""); - }; - - return ( - - - Strikes - - - setSearchInput(e.target.value)} - /> - - {filteredOrgs.length > 0 && ( - - {filteredOrgs.map((org) => ( - { - setOrgId(org.id); - setOrgName(org.name); - setSearchInput(""); // Clear search input after selecting an org - setFilteredOrgs([]); // Clear filtered orgs after selecting an org - }} - sx={{ margin: "5px" }} - > - {org.name} - - ))} - - )} - {orgId && ( - <> - - - Org: {orgName} - - - - Give Strike - - setReason(e.target.value)} - multiline - rows={4} - /> - - Issue - - - - - - {orgStrikes.map((strike, i) => ( - - - - {strike.reason} - - - Issued by {strike.users?.first_name}{" "} - {strike.users?.last_name} - - { - const { error } = await supabase - .from("strikes") - .delete() - .eq("id", strike.id); - - if (error) { - enqueueSnackbar( - "Error deleting strike. Contact it@stuysu.org for support", - { variant: "error" }, - ); - } - - setOrgStrikes( - orgStrikes.filter( - (s) => s.id !== strike.id, - ), - ); - enqueueSnackbar("Strike deleted!", { - variant: "success", - }); - }} - > - Delete - - - - ))} - - - )} - - ); -}; - -export default Strikes; +import { Box, TextField, Typography, Card } from "@mui/material"; +import { useState, useEffect } from "react"; +import { supabase } from "../../supabaseClient"; +import { useSnackbar } from "notistack"; +import AsyncButton from "../../comps/ui/AsyncButton"; + +const Strikes = () => { + const { enqueueSnackbar } = useSnackbar(); + + const [orgId, setOrgId] = useState(); + const [orgName, setOrgName] = useState(""); + const [orgStrikes, setOrgStrikes] = useState([]); + const [reason, setReason] = useState(""); + const [searchInput, setSearchInput] = useState(""); + const [filteredOrgs, setFilteredOrgs] = useState([]); + const [allOrgs, setAllOrgs] = useState([]); + + useEffect(() => { + const fetchAllOrgs = async () => { + const { data, error } = await supabase + .from("organizations") + .select("*"); + if (error || !data) { + enqueueSnackbar( + "Failed to load organizations. Contact it@stuysu.org for support.", + { variant: "error" }, + ); + return; + } + setAllOrgs(data); + }; + + fetchAllOrgs(); + }, [enqueueSnackbar]); + + useEffect(() => { + if (searchInput) { + setFilteredOrgs( + allOrgs.filter((org) => + org.name.toLowerCase().includes(searchInput.toLowerCase()), + ), + ); + } else { + setFilteredOrgs([]); + } + }, [searchInput, allOrgs]); + + useEffect(() => { + if (!orgId) return; + + const fetchOrgStrikes = async () => { + const { data, error } = await supabase + .from("strikes") + .select( + ` + id, + reason, + created_at, + organizations (name), + users (first_name, last_name, picture) + `, + ) + .eq("organization_id", orgId); + + if (error || !data) { + return enqueueSnackbar( + "Failed to load strikes. Contact it@stuysu.org for support.", + { variant: "error" }, + ); + } + + setOrgStrikes(data as Strike[]); + }; + + fetchOrgStrikes(); + }, [orgId, enqueueSnackbar]); + + const issueStrike = async () => { + const { data, error } = await supabase.functions.invoke( + "issue-strike", + { + body: { + organization_id: orgId, + reason, + }, + }, + ); + + if (error) { + enqueueSnackbar( + "Error issuing strike. Contact it@stuysu.org for support", + { variant: "error" }, + ); + return; + } + + setOrgStrikes([...orgStrikes, data as Strike]); + enqueueSnackbar("Strike issued!", { variant: "success" }); + setReason(""); + }; + + return ( + + + Strikes + + + setSearchInput(e.target.value)} + /> + + {filteredOrgs.length > 0 && ( + + {filteredOrgs.map((org) => ( + { + setOrgId(org.id); + setOrgName(org.name); + setSearchInput(""); + setFilteredOrgs([]); + }} + sx={{ margin: "5px" }} + > + {org.name} + + ))} + + )} + {orgId && ( + <> + + + Org: {orgName} + + + + Give Strike + + setReason(e.target.value)} + multiline + rows={4} + /> + + Issue + + + + + + {orgStrikes.map((strike, i) => ( + + + + {strike.reason} + + + Issued by {strike.users?.first_name}{" "} + {strike.users?.last_name} + + { + const { error } = await supabase + .from("strikes") + .delete() + .eq("id", strike.id); + + if (error) { + enqueueSnackbar( + "Error deleting strike. Contact it@stuysu.org for support", + { variant: "error" }, + ); + } + + setOrgStrikes( + orgStrikes.filter( + (s) => s.id !== strike.id, + ), + ); + enqueueSnackbar("Strike deleted!", { + variant: "success", + }); + }} + > + Delete + + + + ))} + + + )} + + ); +}; + +export default Strikes; diff --git a/src/pages/admin/index.tsx b/src/pages/admin/index.tsx index da3be39..c4fc30f 100644 --- a/src/pages/admin/index.tsx +++ b/src/pages/admin/index.tsx @@ -1,39 +1,41 @@ -import { Routes, Route } from "react-router-dom"; - -import { useContext } from "react"; -import UserContext from "../../comps/context/UserContext"; - -import AdminNav from "../../comps/admin/AdminNav"; - -/* MODULES */ -import ApprovePending from "./ApprovePending"; -import ApproveEdit from "./ApproveEdit"; -import Strikes from "./Strikes"; -import SendMessage from "./SendMessage"; -import Announcements from "./Announcements"; -import Rooms from "./Rooms"; - -const AdminRouter = () => { - const user = useContext(UserContext); - - if (!user.admin) { - return
You do not have access to this page.
; - } - - return ( -
- - - - - - - - - - -
- ); -}; - -export default AdminRouter; +import { Routes, Route } from "react-router-dom"; + +import { useContext } from "react"; +import UserContext from "../../comps/context/UserContext"; + +import AdminNav from "../../comps/admin/AdminNav"; + +/* MODULES */ +import ApprovePending from "./ApprovePending"; +import ApproveEdit from "./ApproveEdit"; +import Strikes from "./Strikes"; +import SendMessage from "./SendMessage"; +import Announcements from "./Announcements"; +import Rooms from "./Rooms"; +import ClubAdminEmails from "./ClubAdminEmails"; + +const AdminRouter = () => { + const user = useContext(UserContext); + + if (!user.admin) { + return
You do not have access to this page.
; + } + + return ( +
+ + + + + + + + + + + +
+ ); +}; + +export default AdminRouter; diff --git a/src/pages/orgs/Posts.tsx b/src/pages/orgs/Posts.tsx new file mode 100644 index 0000000..db36769 --- /dev/null +++ b/src/pages/orgs/Posts.tsx @@ -0,0 +1,38 @@ +import { useContext, useState, useEffect } from "react"; +import OrgContext from "../../comps/context/OrgContext"; +import { Box, Grid, Typography } from "@mui/material"; +import Post from "../../comps/pages/orgs/Post"; + +const Posts = () => { + const [posts, setPosts] = useState([]); + + const organization: OrgContextType = useContext(OrgContext); + + useEffect(() => { + setPosts(organization.posts.reverse()); + }, []); + + return ( + + + Posts + + + {posts.map((post) => ( + + + + + + ))} + + + ); +}; + +export default Posts; diff --git a/src/pages/orgs/admin/MemberRequests.tsx b/src/pages/orgs/admin/MemberRequests.tsx index 29e3c26..2dc40fd 100644 --- a/src/pages/orgs/admin/MemberRequests.tsx +++ b/src/pages/orgs/admin/MemberRequests.tsx @@ -1,40 +1,67 @@ -import { useContext } from "react"; -import OrgContext from "../../../comps/context/OrgContext"; - -import PendingMember from "../../../comps/pages/orgs/admin/PendingMember"; -import { Box, Typography } from "@mui/material"; - -const MemberRequests = () => { - const organization = useContext(OrgContext); - const pendingMembers = organization.memberships - ?.filter((member) => !member.active) - .map((member) => { - return { - first_name: member.users?.first_name, - last_name: member.users?.last_name, - email: member.users?.email, - membershipId: member.id, - picture: member.users?.picture, - }; - }); - - return ( - - - Member Requests - - {pendingMembers?.map((member, i) => ( - - ))} - - ); -}; - -export default MemberRequests; +import { useContext, useState } from "react"; +import OrgContext from "../../../comps/context/OrgContext"; + +import PendingMember from "../../../comps/pages/orgs/admin/PendingMember"; +import { Box, Typography } from "@mui/material"; +import AsyncButton from "../../../comps/ui/AsyncButton"; + +const MemberRequests = () => { + const organization = useContext(OrgContext); + const [autoApprove, setAutoApprove] = useState(false); + + const pendingMembers = organization.memberships + ?.filter((member) => !member.active) + .map((member) => { + return { + first_name: member.users?.first_name, + last_name: member.users?.last_name, + email: member.users?.email, + membershipId: member.id, + picture: member.users?.picture, + }; + }); + + const handleApproveAll = () => { + setAutoApprove(true); + }; + + return ( + + + Member Requests + + + + Approve ALL + + + + {pendingMembers?.map((member, i) => ( + + ))} + + ); +}; + +export default MemberRequests; diff --git a/src/pages/orgs/index.tsx b/src/pages/orgs/index.tsx index 5e9d577..efe3275 100644 --- a/src/pages/orgs/index.tsx +++ b/src/pages/orgs/index.tsx @@ -1,188 +1,190 @@ -/* ORG ROUTING INFORMATION HERE */ -import { useEffect, useState } from "react"; -import { Routes, Route, useParams, useNavigate } from "react-router-dom"; -import OrgContext from "../../comps/context/OrgContext"; -import Loading from "../../comps/ui/Loading"; -import { supabase } from "../../supabaseClient"; - -import OrgNav from "../../comps/pages/orgs/OrgNav"; - -import NotFound from "./NotFound"; -import Overview from "./Overview"; -import Charter from "./Charter"; -import Meetings from "./Meetings"; -import Members from "./Members"; -import OrgAdminRouter from "./admin"; -import { useSnackbar } from "notistack"; -import { Box, useMediaQuery } from "@mui/material"; -import AsyncButton from "../../comps/ui/AsyncButton"; - -const OrgRouter = () => { - const { enqueueSnackbar } = useSnackbar(); - const { orgUrl } = useParams(); - - const isMobile = useMediaQuery("(max-width: 1000px)"); - const navigate = useNavigate(); - - const [org, setOrg] = useState({ - id: -1, - name: "", - url: "", - picture: "", - mission: "", - purpose: "", - goals: "", - appointment_procedures: "", - uniqueness: "", - meeting_description: "", - meeting_schedule: "", - meeting_days: [], - commitment_level: "NONE", - state: "PENDING", - joinable: false, - join_instructions: "", - memberships: [], - meetings: [], - posts: [], - }); - const [loading, setLoading] = useState(true); - - useEffect(() => { - const getOrgData = async () => { - const { data, error } = await supabase - .from("organizations") - .select( - ` - id, - name, - socials, - url, - picture, - purpose, - goals, - appointment_procedures, - uniqueness, - meeting_description, - meeting_schedule, - meeting_days, - commitment_level, - keywords, - tags, - faculty_email, - fair, - state, - joinable, - join_instructions, - memberships ( - id, - role, - role_name, - active, - users ( - id, - first_name, - last_name, - email, - picture, - is_faculty - ) - ), - meetings ( - id, - is_public, - title, - description, - start_time, - end_time, - rooms ( - id, - name, - floor - ) - ), - posts ( - id, - title, - description, - created_at, - updated_at, - organizations ( - name, - picture, - id - ) - ) - `, - ) - .ilike("url", orgUrl || ""); - - if (error) { - enqueueSnackbar("Error fetching organization.", { - variant: "error", - }); - return; - } - - if (data?.length === 0) { - return; - } - - setOrg(data[0] as OrgContextType); - }; - - getOrgData().then(() => setLoading(false)); - }, [orgUrl, enqueueSnackbar]); - - if (loading) return ; - return ( - - {org.id === -1 ? ( - - ) : ( - <> - - navigate("/catalog")} - variant="contained" - sx={{ width: "80px" }} - > - Back - - - - - - - - - - - - - - - - )} - - ); -}; - -export default OrgRouter; +/* ORG ROUTING INFORMATION HERE */ +import { useEffect, useState } from "react"; +import { Routes, Route, useParams, useNavigate } from "react-router-dom"; +import OrgContext from "../../comps/context/OrgContext"; +import Loading from "../../comps/ui/Loading"; +import { supabase } from "../../supabaseClient"; + +import OrgNav from "../../comps/pages/orgs/OrgNav"; + +import NotFound from "./NotFound"; +import Overview from "./Overview"; +import Charter from "./Charter"; +import Meetings from "./Meetings"; +import Members from "./Members"; +import Posts from "./Posts"; +import OrgAdminRouter from "./admin"; +import { useSnackbar } from "notistack"; +import { Box, useMediaQuery } from "@mui/material"; +import AsyncButton from "../../comps/ui/AsyncButton"; + +const OrgRouter = () => { + const { enqueueSnackbar } = useSnackbar(); + const { orgUrl } = useParams(); + + const isMobile = useMediaQuery("(max-width: 1000px)"); + const navigate = useNavigate(); + + const [org, setOrg] = useState({ + id: -1, + name: "", + url: "", + picture: "", + mission: "", + purpose: "", + goals: "", + appointment_procedures: "", + uniqueness: "", + meeting_description: "", + meeting_schedule: "", + meeting_days: [], + commitment_level: "NONE", + state: "PENDING", + joinable: false, + join_instructions: "", + memberships: [], + meetings: [], + posts: [], + }); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const getOrgData = async () => { + const { data, error } = await supabase + .from("organizations") + .select( + ` + id, + name, + socials, + url, + picture, + purpose, + goals, + appointment_procedures, + uniqueness, + meeting_description, + meeting_schedule, + meeting_days, + commitment_level, + keywords, + tags, + faculty_email, + fair, + state, + joinable, + join_instructions, + memberships ( + id, + role, + role_name, + active, + users ( + id, + first_name, + last_name, + email, + picture, + is_faculty + ) + ), + meetings ( + id, + is_public, + title, + description, + start_time, + end_time, + rooms ( + id, + name, + floor + ) + ), + posts ( + id, + title, + description, + created_at, + updated_at, + organizations ( + name, + picture, + id + ) + ) + `, + ) + .ilike("url", orgUrl || ""); + + if (error) { + enqueueSnackbar("Error fetching organization.", { + variant: "error", + }); + return; + } + + if (data?.length === 0) { + return; + } + + setOrg(data[0] as OrgContextType); + }; + + getOrgData().then(() => setLoading(false)); + }, [orgUrl, enqueueSnackbar]); + + if (loading) return ; + return ( + + {org.id === -1 ? ( + + ) : ( + <> + + navigate("/catalog")} + variant="contained" + sx={{ width: "80px" }} + > + Back + + + + + + + + + + + + + + + + + )} + + ); +}; + +export default OrgRouter;