diff --git a/src/components/ui/forms/FormTagSelect.tsx b/src/components/ui/forms/FormTagSelect.tsx index 88f395d7..ecafbfdf 100644 --- a/src/components/ui/forms/FormTagSelect.tsx +++ b/src/components/ui/forms/FormTagSelect.tsx @@ -78,7 +78,7 @@ const FormTagSelect = ({ onChange={valueChanged} value={value} options={tags} - renderTags={(value, props) => { + renderTags={(value: string[], props) => { return value.map((option, index) => ( )); diff --git a/src/components/ui/forms/FormTextField.tsx b/src/components/ui/forms/FormTextField.tsx index 75bce774..20aca79f 100644 --- a/src/components/ui/forms/FormTextField.tsx +++ b/src/components/ui/forms/FormTextField.tsx @@ -24,6 +24,7 @@ type Props = { }; changeStatus?: (field: string, newStatus: boolean) => void; hideHelper?: boolean; + textHelper?: string; }; const FormTextField = ({ @@ -36,6 +37,10 @@ const FormTextField = ({ status, changeStatus, hideHelper, + textHelper, + label, + sx, + error, ...textFieldProps }: Props & TextFieldProps) => { useEffect(() => { @@ -80,7 +85,6 @@ const FormTextField = ({ const textChanged = (event: ChangeEvent) => { let targetValue = event.target.value; - if ( requirements?.maxChar && targetValue.length > requirements.maxChar @@ -90,7 +94,7 @@ const FormTextField = ({ if ( requirements?.maxWords && targetValue.replace(/ +/g, " ").split(" ").length > - requirements.maxWords + requirements.maxWords ) { targetValue = targetValue .replace(/ +/g, " ") @@ -129,21 +133,21 @@ const FormTextField = ({ let countChars = false; if (requirements.minChar) { countChars = true; - helperText += `: min ${requirements.minChar} Characters`; + helperText += `: Minimum ${requirements.minChar} Characters`; } - if (requirements.maxChar) { + if (requirements.maxChar && !description) { if (countChars) { - helperText += " to"; + helperText += " to "; } else { helperText += ":"; } countChars = true; - helperText += ` max ${requirements.maxChar} Characters`; + helperText += `Maximum ${requirements.maxChar} Characters`; } - if (countChars) helperText += `, at ${value?.length || 0}.`; + if (countChars) helperText += ` (${value?.length || 0}/${requirements.maxChar}).`; let countWords = false; @@ -153,7 +157,7 @@ const FormTextField = ({ } countWords = true; - helperText += ` min ${requirements.minWords} Words`; + helperText += ` Minimum ${requirements.minWords} Words`; } if (requirements.maxWords) { @@ -162,36 +166,46 @@ const FormTextField = ({ } if (countWords) { - helperText += " to"; + helperText += " to "; } countWords = true; - helperText += ` max ${requirements.maxWords} Words`; + helperText += `Maximum ${requirements.maxWords} Words`; } if (countWords) - helperText += `, at ${value?.trim().split(" ").length || 0}.`; + helperText += ` (${value?.trim().split(" ").length || 0}/${requirements.maxWords})`; } - return ( - {description?.split("\n").map((line) => ( - <> + {/* description */} + {description?.split("\n").map((line, idx) => ( + {line}
- +
+ ))} + + {/* internal helper text (requirements like min/max chars/words) */} + {textHelper?.split("\n").map((line, idx) => ( + + {line} +
+
))} - {helperText} ) } + label={label} + sx={sx} + {...textFieldProps} /> ); diff --git a/src/components/ui/forms/UnifiedChipSelector.tsx b/src/components/ui/forms/UnifiedChipSelector.tsx new file mode 100644 index 00000000..13987d47 --- /dev/null +++ b/src/components/ui/forms/UnifiedChipSelector.tsx @@ -0,0 +1,70 @@ +import React from 'react' +import FormChipText from './FormChipText' +import OrgRequirements from '../../../utils/OrgRequirements'; +import FormTagSelect from './FormTagSelect'; +import { SxProps } from '@mui/material'; + +type Requirements = { + [field: string]: any; +}; + +type Props = { + field: string; + description?: string; + required?: boolean; + requirements?: Requirements; + value?: string | string[]; + value_delim?: string; + onChange?: (updatedValue: string[]) => void; + status?: { + dirty: boolean; + value: boolean; + }; + changeStatus?: (field: string, newStatus: boolean) => void; + label?: string; + options?: string[]; + sx?: SxProps; +}; +const UnifiedChipSelector = ({ + field, + label, + onChange, + value, + changeStatus, + value_delim, + options, + description, + sx, +}: Props) => { + if (typeof value === 'string') value = value.split(value_delim!); + const thereAreOptions = !!options; + if (!thereAreOptions) { + return ( + + + ) + } else { + return ( + + ) + } +} + +export default UnifiedChipSelector; diff --git a/src/modules/stuyactivities/CharterForm.tsx b/src/modules/stuyactivities/CharterForm.tsx index a1720d87..6b51ea90 100644 --- a/src/modules/stuyactivities/CharterForm.tsx +++ b/src/modules/stuyactivities/CharterForm.tsx @@ -358,7 +358,7 @@ const CharterForm = () => { sx={{ width: isMobile ? "100%" : "50%" }} /> \nExample: https://epsilon.stuysu.org/suit" @@ -371,22 +371,26 @@ const CharterForm = () => { width: isMobile ? "100%" : "50%", }} error={!!urlError} - helperText={ - urlError + textHelper={ + (`\nCurrently, the web page to access your Activity would be https://epsilon.stuysu.org/${formData.url}. \n`) + + (urlError ? urlError : checkingUrl - ? "Checking URL..." - : formData.url && !urlError - ? "URL is valid." - : undefined + ? "Checking URL..." + : formData.url && !urlError + ? "URL is valid." + : "") } + + onBlur={async (e: any) => { const url = e.target.value; if (url) await validateUrl(url); + if (url.length < 1) setUrlError(""); }} onChange={(e: any) => { const url = e.target.value; - if (url === "" || urlPattern.test(url)) { + if (url.length < 1 || urlPattern.test(url)) { setFormData((prev) => ({ ...prev, url })); setUrlError(""); } else { @@ -396,6 +400,7 @@ const CharterForm = () => { } }} inputProps={{ pattern: urlPattern.source }} + hideHelper={false} /> ) => { const { enqueueSnackbar } = useSnackbar(); const [buttonsDisabled, setButtonsDisabled] = useState(false); - + const creator = org.memberships?.find((m) => m.role === 'CREATOR'); + let keywords = org.keywords?.split(","); const approve = async () => { setButtonsDisabled(true); const { error } = await supabase.functions.invoke( @@ -163,7 +166,12 @@ const OrgApproval = ({

Keywords

-

{org.keywords || "none"}

+ {!org.keywords &&

none

} + {org.keywords && + keywords?.map((n, i) => ( +

{acronyms.includes(n.toUpperCase()) ? n.toUpperCase() : n[0].toUpperCase() + n.slice(1)}

+ )) + }

Tags

@@ -175,11 +183,9 @@ const OrgApproval = ({

Creator

+

{creator?.users?.first_name} {" "} {creator?.users?.last_name}

- { - org.memberships?.find((m) => m.role === "CREATOR") - ?.users?.email - } + {creator?.users?.email}

diff --git a/src/modules/stuyactivities/admin/pages/ApprovePending.tsx b/src/modules/stuyactivities/admin/pages/ApprovePending.tsx index 94986c3b..fb229c41 100644 --- a/src/modules/stuyactivities/admin/pages/ApprovePending.tsx +++ b/src/modules/stuyactivities/admin/pages/ApprovePending.tsx @@ -56,8 +56,11 @@ const ApprovePending = () => { ) ) `, - ) - .eq("state", "PENDING"); + ).order('id', {ascending:true}); + + const count = (await supabase.from("organizations").select('*').order('id', {ascending: true})).data; + console.log(count![0]); + if (error || !data) { return enqueueSnackbar( diff --git a/src/modules/stuyactivities/orgs/org_admin/components/OrgEditor.tsx b/src/modules/stuyactivities/orgs/org_admin/components/OrgEditor.tsx index 7dac43b4..2aca4a10 100644 --- a/src/modules/stuyactivities/orgs/org_admin/components/OrgEditor.tsx +++ b/src/modules/stuyactivities/orgs/org_admin/components/OrgEditor.tsx @@ -1,5 +1,5 @@ import { ReactNode, useCallback, useEffect, useState } from "react"; -import { Avatar, Box, Paper } from "@mui/material"; +import { Avatar, Box, Paper, useMediaQuery } from "@mui/material"; import { useSnackbar } from "notistack"; import FormTextField from "../../../../../components/ui/forms/FormTextField"; import OrgRequirements from "../../../../../utils/OrgRequirements"; @@ -8,6 +8,10 @@ import { supabase } from "../../../../../lib/supabaseClient"; import { PUBLIC_URL } from "../../../../../config/constants"; import { useNavigate } from "react-router-dom"; import AsyncButton from "../../../../../components/ui/buttons/AsyncButton"; +import FormTagSelect from "../../../../../components/ui/forms/FormTagSelect"; +import FormSection from "../../../../../components/ui/forms/FormSection"; +import FormChipText from "../../../../../components/ui/forms/FormChipText"; +import UnifiedChipSelector from "../../../../../components/ui/forms/UnifiedChipSelector"; type Props = { organization: Partial; // Make organization a prop to allow component to become reusable @@ -28,6 +32,12 @@ type FormStatus = { type orgKey = keyof OrganizationEdit & keyof Organization; +function formatStr(a: string | string[] | undefined) { + let final_value = a; + final_value = (typeof a == "object" ? + Array.from(a).join('\n') : a) + return final_value; +} const EditField = ({ field, pending, @@ -127,6 +137,17 @@ const hiddenFields: string[] = [ "picture", // picture field has custom logic ]; +function arraysEqual(a: string[], b: string[]) { + if (a === b) return true; + if (a == null || b == null) return false; + if (a.length !== b.length) return false; + + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; +} + /* TextField Statuses: - default is Approved @@ -142,7 +163,7 @@ const OrgEditor = ({ setPendingEdit, }: Props) => { const { enqueueSnackbar } = useSnackbar(); - + const isMobile = useMediaQuery("(max-width: 640px)"); /* everything that is displayed on the page (could include values similar to original)*/ const [editData, setEditData] = useState({}); @@ -156,9 +177,11 @@ const OrgEditor = ({ File | null | undefined | string >(); + const [editTags, setEditTags] = useState(null); + const oldPicture = existingEdit["picture"] === undefined || - existingEdit["picture"] === null + existingEdit["picture"] === null ? organization["picture"] : existingEdit["picture"]; @@ -176,6 +199,11 @@ const OrgEditor = ({ return; } + if (editTags !== null && !arraysEqual(editTags, organization.tags!)) { + setSavable(true); + return; + } + /* check if there is anything to save, if not, terminate */ let editedFields: string[] = Object.keys(editState); if (!editedFields.length) { @@ -233,8 +261,13 @@ const OrgEditor = ({ } setEditData(baseData); + console.log(organization); }, [existingEdit, organization]); + useEffect(() => { + console.log("change:", editData); + }, [editData]); + const saveChanges = async () => { if (!savable) { enqueueSnackbar( @@ -299,9 +332,9 @@ const OrgEditor = ({ allNull = false; // even though all the fields could be null, this edit is worth keeping because the picture is different } else if ( editPicture !== - (existingEdit === undefined - ? organization["picture"] - : existingEdit["picture"]) && + (existingEdit === undefined + ? organization["picture"] + : existingEdit["picture"]) && editPicture ) { // picture is different, but needs to be uploaded first @@ -392,10 +425,11 @@ const OrgEditor = ({ } let data, error; - + console.log(payload); if (organization.state !== "PENDING") { if (existingEdit.id === undefined) { // insert new + ({ data, error } = await supabase .from("organizationedits") .insert({ organization_id: organization.id, ...payload }) @@ -417,7 +451,6 @@ const OrgEditor = ({ delete payload[key]; } } - ({ data, error } = await supabase .from("organizations") .update(payload) @@ -426,6 +459,7 @@ const OrgEditor = ({ } if (error || !data) { + console.error(error); return enqueueSnackbar( "Error editing organization. Contact it@stuysu.org for support.", { variant: "error" }, @@ -497,6 +531,9 @@ const OrgEditor = ({ ); const updateEdit = (field: keyof OrganizationEdit, value: any) => { + if (field == "tags") { + setEditTags(value); + } setEditData({ ...editData, [field]: value, @@ -564,9 +601,9 @@ const OrgEditor = ({ if ( e.target.files[0].size > 1024 * - 1024 * - OrgRequirements.picture?.requirements - ?.maxSize[0] + 1024 * + OrgRequirements.picture?.requirements + ?.maxSize[0] ) { return enqueueSnackbar( `File is too large. Max size is ${OrgRequirements.picture?.requirements?.maxSize[0]}MB.`, @@ -621,7 +658,7 @@ const OrgEditor = ({ (existingEdit[field as keyof OrganizationEdit] !== null && existingEdit[ - field as keyof OrganizationEdit + field as keyof OrganizationEdit ] !== undefined) || organization.state === "PENDING" } @@ -631,7 +668,7 @@ const OrgEditor = ({ updateEdit( field as keyof OrganizationEdit, existingEdit[field as keyof OrganizationEdit] || - organization[field as keyof Organization], + organization[field as keyof Organization], ); // remove self from list of fields being edited @@ -652,8 +689,8 @@ const OrgEditor = ({ setEditState({ ...editState, [field]: true }) } defaultDisplay={ -

- {editData[field as keyof OrganizationEdit] || ( +

+ {formatStr(editData[field as keyof OrganizationEdit]) || ( <empty> )}

@@ -670,7 +707,7 @@ const OrgEditor = ({ ) } value={ - editData[field as keyof OrganizationEdit] + formatStr(editData[field as keyof OrganizationEdit]) } required={OrgRequirements[field].required} requirements={ @@ -684,6 +721,61 @@ const OrgEditor = ({ /> ); })} + + { + updateEdit( + "keywords" as keyof OrganizationEdit, + val.join(","), + ) + }} + value={editData.keywords?.split(",") ?? []} + required={OrgRequirements.keywords.required} + requirements={OrgRequirements.keywords.requirements} + description={`You are allowed up to 3 keywords that describe your Activity. These will not be publicly visible but will help your Activity show up in search results. Examples of keywords include alternate names or acronyms, such as 'SU' for the Student Union. Create a keyword using or <,>. PLEASE NOTE: You cannot paste a list of keywords, you must type them manually.`} + /> + + + updateEdit( + "tags" as keyof OrganizationEdit, + val, + ) + } + /> + +