From b0ee22779ad6efcb17bd7ea8c4b17b24a4622de5 Mon Sep 17 00:00:00 2001 From: owlester12 <64493239+owlester12@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:01:46 -0400 Subject: [PATCH 1/3] Rename club description to mission (#738) Rebrands club description field to club mission cosmetically --------- Co-authored-by: Julian Weng <julian.weng.us@gmail.com> --- frontend/components/ClubCard.tsx | 4 +++- frontend/components/ClubEditPage/ClubEditCard.tsx | 3 ++- frontend/components/ClubPage/Description.tsx | 2 +- frontend/components/EmbedOption.tsx | 5 ++--- frontend/pages/club/[club]/renew.tsx | 2 +- frontend/pages/rank.tsx | 8 ++++---- frontend/utils.tsx | 2 +- 7 files changed, 14 insertions(+), 12 deletions(-) diff --git a/frontend/components/ClubCard.tsx b/frontend/components/ClubCard.tsx index 80254bd87..4587aee50 100644 --- a/frontend/components/ClubCard.tsx +++ b/frontend/components/ClubCard.tsx @@ -100,7 +100,9 @@ const ClubCard = ({ club, fullWidth }: ClubCardProps): ReactElement => { const { name, active, approved, subtitle, tags, enables_subscription, code } = club const img = club.image_url - const textDescription = shorten(subtitle || 'This club has no description.') + const textDescription = shorten( + subtitle || 'This club has not provided a mission statement.', + ) return ( <CardWrapper className={fullWidth ? '' : 'column is-half-desktop'}> diff --git a/frontend/components/ClubEditPage/ClubEditCard.tsx b/frontend/components/ClubEditPage/ClubEditCard.tsx index 9e4509592..c7234e9b6 100644 --- a/frontend/components/ClubEditPage/ClubEditCard.tsx +++ b/frontend/components/ClubEditPage/ClubEditCard.tsx @@ -446,8 +446,9 @@ export default function ClubEditCard({ }, { name: 'description', + label: 'Club Mission', required: true, - placeholder: `Type your ${OBJECT_NAME_SINGULAR} description here!`, + placeholder: `Type your ${OBJECT_NAME_SINGULAR} mission here!`, type: 'html', hidden: !REAPPROVAL_QUEUE_ENABLED, }, diff --git a/frontend/components/ClubPage/Description.tsx b/frontend/components/ClubPage/Description.tsx index 7ce9b30e1..48f1d7231 100644 --- a/frontend/components/ClubPage/Description.tsx +++ b/frontend/components/ClubPage/Description.tsx @@ -20,7 +20,7 @@ type Props = { const Description = ({ club }: Props): ReactElement => ( <Wrapper> <div style={{ width: '100%' }}> - <StrongText>Description</StrongText> + <StrongText>Club Mission</StrongText> <div className="content" dangerouslySetInnerHTML={{ diff --git a/frontend/components/EmbedOption.tsx b/frontend/components/EmbedOption.tsx index 2f4e8c9d1..54af4f99a 100644 --- a/frontend/components/EmbedOption.tsx +++ b/frontend/components/EmbedOption.tsx @@ -184,9 +184,8 @@ const EmbedOption = (props: Props): ReactElement => { <h1>Embed Content</h1> <p> You can use this tool to embed multimedia content into your club - description. If you run into any issues using the tool, please - contact <Contact />. Here are examples of some of the things you can - embed. + mission. If you run into any issues using the tool, please contact{' '} + <Contact />. Here are examples of some of the things you can embed. </p> <div className="content mb-3"> <ul> diff --git a/frontend/pages/club/[club]/renew.tsx b/frontend/pages/club/[club]/renew.tsx index c5304f519..4390583e5 100644 --- a/frontend/pages/club/[club]/renew.tsx +++ b/frontend/pages/club/[club]/renew.tsx @@ -295,7 +295,7 @@ const RenewPage = (props: RenewPageProps): ReactElement => { name: 'University Affiliation', content: ( <div> - The club description must clearly state that the group is a student + The club mission must clearly state that the group is a student organization at the University. </div> ), diff --git a/frontend/pages/rank.tsx b/frontend/pages/rank.tsx index c62277a9d..531d01c2b 100644 --- a/frontend/pages/rank.tsx +++ b/frontend/pages/rank.tsx @@ -235,13 +235,13 @@ const Rank = (): ReactElement => ( ], }, { - name: `${OBJECT_NAME_TITLE} Description`, + name: `${OBJECT_NAME_TITLE} Mission`, description: ( <> - Adding a description helps students learn more about whether or + Adding a club mission helps students learn more about whether or not a {OBJECT_NAME_SINGULAR} is a good fit for them.{' '} - {OBJECT_NAME_TITLE} without a description will therefore appear - lower on the homepage. Longer and more detailed descriptions are + {OBJECT_NAME_TITLE} without a club mission will therefore appear + lower on the homepage. Longer and more detailed club missions are awarded bonus points. </> ), diff --git a/frontend/utils.tsx b/frontend/utils.tsx index e32bfb0ba..bc911d72e 100644 --- a/frontend/utils.tsx +++ b/frontend/utils.tsx @@ -91,7 +91,7 @@ export const SITE_ORIGIN = publicRuntimeConfig.SITE_ORIGIN export const API_BASE_URL = `${SITE_ORIGIN}/api` export const EMPTY_DESCRIPTION = - '<span style="color:#666">This club has not added a description yet.</span>' + '<span style="color:#666">This club has not added a club mission yet.</span>' export const LOGIN_URL = `${API_BASE_URL}/accounts/login/` export const LOGOUT_URL = `${API_BASE_URL}/accounts/logout/` From 109c44c0f43d7cfe15bb234becf4fb07e39e0083 Mon Sep 17 00:00:00 2001 From: Julian Weng <julian.weng.us@gmail.com> Date: Mon, 21 Oct 2024 01:08:36 -0400 Subject: [PATCH 2/3] Move ranking information from separate /rank page to ClubEditPage modal (#741) --- .../components/ClubEditPage/ClubEditCard.tsx | 56 ++- frontend/pages/rank.tsx | 365 ------------------ 2 files changed, 55 insertions(+), 366 deletions(-) delete mode 100644 frontend/pages/rank.tsx diff --git a/frontend/components/ClubEditPage/ClubEditCard.tsx b/frontend/components/ClubEditPage/ClubEditCard.tsx index c7234e9b6..679a310ba 100644 --- a/frontend/components/ClubEditPage/ClubEditCard.tsx +++ b/frontend/components/ClubEditPage/ClubEditCard.tsx @@ -40,8 +40,9 @@ import { SITE_ID, SITE_NAME, } from '../../utils/branding' +import { ModalContent } from '../ClubPage/Actions' import { LiveBanner, LiveSub, LiveTitle } from '../ClubPage/LiveEventsDialog' -import { Checkbox, CheckboxLabel, Contact, Text } from '../common' +import { Checkbox, CheckboxLabel, Contact, Modal, Text } from '../common' import { CheckboxField, CheckboxTextField, @@ -189,6 +190,7 @@ export default function ClubEditCard({ isEdit, onSubmit = () => Promise.resolve(undefined), }: ClubEditCardProps): ReactElement { + const [showRankModal, setShowRankModal] = useState<boolean>(false) const [showTargetFields, setShowTargetFields] = useState<boolean>( !!( club.target_majors?.length || @@ -397,6 +399,58 @@ export default function ClubEditCard({ { name: 'General', type: 'group', + description: ( + <div className="mb-4"> + <a onClick={() => setShowRankModal(true)}> + How does filling out this information affect your club? + </a> + <Modal + show={showRankModal} + closeModal={() => setShowRankModal(false)} + marginBottom={false} + width="80%" + > + <ModalContent className="content mb-4"> + <h2>How we calculate club rankings</h2> + <hr /> + <h5> + The following positively affects your club's ranking in homepage + search results: + </h5> + <ul> + <li> + Upcoming events with filled out name, description, and image + </li> + <li>Upcoming, open applications for membership</li> + <li> + Having at least 3 active officers, plus a bonus for any + additional non-officer member on the platform + </li> + <li> + Having between 3 and 7 useful tags (please email <Contact />{' '} + if none apply) + </li> + <li> + Posting a public (non-personal) contact email and 2 or more + social links + </li> + <li> + Having a club logo image uploaded and subtitle filled out + </li> + <li> + Filling out a club mission with images and detail (rewarded up + to 1000 words) + </li> + <li>Displaying 3 or more student testimonials (experiences)</li> + <li>Filling out the {FIELD_PARTICIPATION_LABEL} section</li> + <li> + Updating the club listing recently (within the last 8 months) + </li> + </ul> + </ModalContent> + </Modal> + </div> + ), fields: [ { name: 'name', diff --git a/frontend/pages/rank.tsx b/frontend/pages/rank.tsx deleted file mode 100644 index 531d01c2b..000000000 --- a/frontend/pages/rank.tsx +++ /dev/null @@ -1,365 +0,0 @@ -import { - Contact, - Container, - Icon, - InfoPageTitle, - Metadata, - StrongText, - Text, -} from 'components/common' -import { ReactElement } from 'react' -import renderPage from 'renderPage' -import styled from 'styled-components' -import { - FIELD_PARTICIPATION_LABEL, - OBJECT_NAME_PLURAL, - OBJECT_NAME_SINGULAR, - OBJECT_NAME_TITLE, - OBJECT_NAME_TITLE_SINGULAR, - SHOW_RANK_ALGORITHM, - SITE_NAME, -} from 'utils/branding' - -import { GREEN, SNOW } from '~/constants/colors' - -const RankItem = styled.div` - padding: 0.75em; - margin-top: 15px; - display: flex; - - & p { - margin-bottom: 0; - } - - & ul { - display: block; - font-size: 0.9em; - margin-left: 1em; - } -` - -const LargeIconWrapper = styled.div` - flex-basis: 80px; - margin-right: 10px; -` - -const LargeIcon = styled(Icon)` - width: 75px; - height: 75px; - padding: 5px; - - @media (max-width: 769px) { - & { - width: 45px; - height: 45px; - } - } -` - -type RankItemData = { - name: string - description: string | ReactElement - points?: [number, string][] -} - -type RankListProps = { - items: RankItemData[] -} - -const RankList = ({ items }: RankListProps): ReactElement => { - return ( - <div className="is-clearfix mb-5"> - {items.map(({ name, description, points }) => ( - <RankItem key={name}> - <LargeIconWrapper> - <LargeIcon - name="check-circle" - alt="check" - style={{ color: GREEN }} - /> - </LargeIconWrapper> - <div> - <b>{name}</b> - <Text>{description}</Text> - {points && ( - <ul> - {points.map(([num, desc], i) => ( - <li key={i}> - <b>{num > 0 ? `+${num}` : num}</b>: {desc} - </li> - ))} - </ul> - )} - </div> - </RankItem> - ))} - </div> - ) -} - -const Rank = (): ReactElement => ( - <Container background={SNOW}> - <Metadata title={`${OBJECT_NAME_TITLE_SINGULAR} Ordering`} /> - <InfoPageTitle> - {OBJECT_NAME_TITLE_SINGULAR} Recommendation Algorithm - </InfoPageTitle> - {SHOW_RANK_ALGORITHM || ( - <div className="notification is-info"> - <Icon name="alert-circle" /> The {OBJECT_NAME_SINGULAR} recommendation - algorithm is not fully configured for {SITE_NAME}. The categories listed - below may or may not be taken into consideration when ordering{' '} - {OBJECT_NAME_PLURAL} on the home page. - </div> - )} - <StrongText>How are {OBJECT_NAME_PLURAL} ordered?</StrongText> - <Text> - The order that {OBJECT_NAME_PLURAL} appear on the home page for the - default ordering method is determined by several criteria. A - recommendation algorithm uses these criteria to ensure that students - receive the best experience when browsing for new {OBJECT_NAME_PLURAL} and - that {OBJECT_NAME_PLURAL} can effectively reach their target demographic. - </Text> - <StrongText> - How does the {OBJECT_NAME_SINGULAR} recommendation algorithm work? - </StrongText> - <Text> - The recommendation algorithm uses the following non-targeted criteria to - determine how to order {OBJECT_NAME_PLURAL} on the home page.{' '} - {OBJECT_NAME_TITLE} are ordered by points, and then this ordering as - adjusted based on personalized data. The points obtained from these - categories is calculated and saved once per day at 4 AM, so make your - changes early! The criteria are: - </Text> - <RankList - items={[ - { - name: 'Upcoming Events', - description: `If your ${OBJECT_NAME_SINGULAR} has upcoming events registered on ${SITE_NAME}, it will be prioritized on the home page a short period before and during the event. Only events shorter than 16 hours are eligible.`, - points: [ - [10, 'Participating in upcoming activities fair'], - [10, 'At least one upcoming event is today'], - [ - 10, - 'All upcoming events today have a complete picture and description', - ], - [5, 'At least one upcoming event in the next week'], - [ - 5, - 'All upcoming events this week have a complete picture and description', - ], - ], - }, - { - name: 'Upcoming Applications', - description: `If a ${OBJECT_NAME_SINGULAR} application is currently open for your ${OBJECT_NAME_SINGULAR}, it will be prioritized while that application is still open.`, - points: [ - [25, `Has at least one open ${OBJECT_NAME_SINGULAR} application`], - ], - }, - { - name: 'Membership', - description: `Having your ${OBJECT_NAME_SINGULAR} members displayed on ${SITE_NAME} provides more points of contact for questions about your ${OBJECT_NAME_SINGULAR}.`, - points: [ - [15, 'At least 3 active officers'], - [10, 'At least 3 active members'], - [0.1, 'For every non-officer member'], - ], - }, - { - name: 'Useful Tags', - description: ( - <> - Adding relevant tags to your {OBJECT_NAME_SINGULAR} can help - prospective students find the {OBJECT_NAME_PLURAL} that they are - interested in. If you cannot find at least 2 relevant tags for - your {OBJECT_NAME_SINGULAR}, please email <Contact /> and we will - work with you to find something appropriate. - </> - ), - points: [ - [15, 'Has anywhere between 3 and 7 tags'], - [7, 'Has more than 7 tags'], - ], - }, - { - name: 'Contact Information', - description: ( - <> - Having contact information is important for prospective members - who want to know more about the {OBJECT_NAME_SINGULAR}. Social - links can be used to give students a better idea of what you do - and the events that you hold. - </> - ), - points: [ - [10, 'Has a public email'], - [10, 'Has 2 or more social links'], - ], - }, - { - name: 'Bookmarks', - description: ( - <> - Bookmarks are a method for Penn students to show interest in your - {OBJECT_NAME_SINGULAR}. The more bookmarks you have, the higher - your {OBJECT_NAME_SINGULAR} will appear. - </> - ), - points: [[0.04, 'For each bookmark']], - }, - { - name: 'Logo Image', - description: ( - <> - Adding a logo to your {OBJECT_NAME_SINGULAR} can make your{' '} - {OBJECT_NAME_SINGULAR} more recognizable. The logo is shown on the - homepage before the user clicks on your {OBJECT_NAME_SINGULAR}. - </> - ), - points: [[15, 'Has a logo']], - }, - { - name: `${OBJECT_NAME_TITLE} Subtitle`, - description: ( - <> - Adding a subtitle is a quick change that can give students more - information about your {OBJECT_NAME_SINGULAR} without having to - visit your {OBJECT_NAME_SINGULAR} - page. The subtitle is shown on the homepage before the user clicks - on your {OBJECT_NAME_SINGULAR}. - </> - ), - points: [ - [5, 'Has a subtitle'], - [-10, 'Did not change default subtitle'], - ], - }, - { - name: `${OBJECT_NAME_TITLE} Mission`, - description: ( - <> - Adding a club mission helps students learn more about whether or - not a {OBJECT_NAME_SINGULAR} is a good fit for them.{' '} - {OBJECT_NAME_TITLE} without a club mission will therefore appear - lower on the homepage. Longer and more detailed club missions are - awarded bonus points. - </> - ), - points: [ - [10, 'At least 25 characters'], - [10, 'At least 250 characters'], - [10, 'At least 1000 characters'], - [3, 'Having images in your description'], - ], - }, - { - name: 'Student Experiences', - description: ( - <> - Adding some testimonials help students gain perspective on what - participating in the {OBJECT_NAME_SINGULAR} is like. - </> - ), - points: [ - [10, 'At least one testimonial'], - [5, 'At least 3 testimonials'], - ], - }, - { - name: FIELD_PARTICIPATION_LABEL, - description: `Prospective members want to know how to participate in your ${OBJECT_NAME_SINGULAR}. Omitting this section will result in a large ordering penalty.`, - points: [[-30, `Empty ${FIELD_PARTICIPATION_LABEL} section`]], - }, - { - name: `Is ${OBJECT_NAME_TITLE_SINGULAR} Updated`, - description: `${OBJECT_NAME_TITLE} that have not been updated in the last 8 months will receive a small ordering penalty.`, - points: [[-10, 'No updates for 8 months']], - }, - { - name: `Is ${OBJECT_NAME_TITLE_SINGULAR} Active`, - description: ( - <> - {OBJECT_NAME_TITLE} that are marked as inactive will be shifted to - the very bottom of the list. You can easily renew your{' '} - {OBJECT_NAME_SINGULAR} from the settings tab in the manage{' '} - {OBJECT_NAME_SINGULAR} page. - </> - ), - points: [[-1000, `For inactive ${OBJECT_NAME_PLURAL}`]], - }, - { - name: 'Random Factor', - description: `A random factor is applied periodically in order to ensure that students see new ${OBJECT_NAME_PLURAL} when they visit ${SITE_NAME}.`, - points: [ - [ - 25, - 'Standard exponential random number scaled to average this number, updated daily', - ], - ], - }, - ]} - /> - <Text> - The algorithm also attempts to personalize search results for logged in - users, based on the following criteria: - </Text> - <RankList - items={[ - { - name: 'Matches Target Tags', - description: ( - <> - Adding tags will case the {OBJECT_NAME_SINGULAR} to appear higher - on the home page for students who are interested in those tags.{' '} - {OBJECT_NAME_TITLE} that have specified fewer tags are more likely - to appear higher than {OBJECT_NAME_PLURAL} that have specified - more tags, for relevant students. - </> - ), - }, - { - name: 'Matches Target Schools', - description: ( - <> - Adding target schools will cause the {OBJECT_NAME_SINGULAR} to - appear higher on the home page for students in those schools.{' '} - {OBJECT_NAME_TITLE} that have specified fewer schools are more - likely to appear higher than {OBJECT_NAME_PLURAL} that have - specified more schools, for relevant students. Specifying all of - the schools is the same as specifying none of them. - </> - ), - }, - { - name: 'Matches Target Majors', - description: ( - <> - Adding target majors will cause the {OBJECT_NAME_SINGULAR} to - appear higher on the home page for students in those majors.{' '} - {OBJECT_NAME_TITLE} that have specified fewer majors are more - likely to appear higher than {OBJECT_NAME_PLURAL} that have - specified more majors, for relevant students. Specifying 10 or - more majors is the same as specifying no majors. - </> - ), - }, - { - name: 'Matches Target Years', - description: ( - <> - Adding target years will cause the {OBJECT_NAME_SINGULAR} to - appear higher on the home page for students in those years.{' '} - {OBJECT_NAME_TITLE} - that have specified fewer years are more likely to appear higher - than {OBJECT_NAME_PLURAL} that have specified more years, for - relevant students. Specifying all of the years is the same as - specifying none of them. - </> - ), - }, - ]} - /> - </Container> -) - -export default renderPage(Rank) From 63c7a7dbe33a2e9622e559acbfd0c96f3c13dd7b Mon Sep 17 00:00:00 2001 From: Julian Weng <julian.weng.us@gmail.com> Date: Fri, 25 Oct 2024 18:03:13 -0400 Subject: [PATCH 3/3] Fix Wharton Council cycle application extension functionality --- backend/clubs/views.py | 51 +++++++++++- .../Settings/WhartonApplicationCycles.tsx | 81 ++++++++++++++----- 2 files changed, 111 insertions(+), 21 deletions(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index c71dd1c65..d8a127c62 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -6562,6 +6562,52 @@ def remove_clubs_from_exception(self, *args, **kwargs): ) return Response([]) + @action(detail=True, methods=["GET"]) + def club_applications(self, *args, **kwargs): + """ + Retrieve club applications for given cycle + --- + requestBody: + content: {} + responses: + "200": + content: + application/json: + schema: + type: array + items: + type: object + properties: + name: + type: string + id: + type: integer + application_end_time: + type: string + format: date-time + application_end_time_exception: + type: string + club__name: + type: string + club__code: + type: string + --- + """ + cycle = self.get_object() + + return Response( + ClubApplication.objects.filter(application_cycle=cycle) + .select_related("club") + .values( + "name", + "id", + "application_end_time", + "application_end_time_exception", + "club__name", + "club__code", + ) + ) + @action(detail=True, methods=["GET"]) def applications(self, *args, **kwargs): """ @@ -6570,7 +6616,10 @@ def applications(self, *args, **kwargs): requestBody: {} responses: "200": - content: {} + content: + text/csv: + schema: + type: string --- """ cycle = self.get_object() diff --git a/frontend/components/Settings/WhartonApplicationCycles.tsx b/frontend/components/Settings/WhartonApplicationCycles.tsx index 88bae600b..223b36ad0 100644 --- a/frontend/components/Settings/WhartonApplicationCycles.tsx +++ b/frontend/components/Settings/WhartonApplicationCycles.tsx @@ -13,16 +13,17 @@ import ModelForm from '../ModelForm' const fields = ( <> - <Field name="name" as={TextField} /> - <Field name="start_date" as={DateTimeField} /> - <Field name="end_date" as={DateTimeField} /> - <Field name="release_date" as={DateTimeField} /> + <Field name="name" as={TextField} required /> + <Field name="start_date" as={DateTimeField} required /> + <Field name="end_date" as={DateTimeField} required /> + <Field name="release_date" as={DateTimeField} required /> </> ) type Cycle = { name: string id: number | null + endDate: Date } type ClubOption = { @@ -35,7 +36,8 @@ type ExtensionOption = { clubName: string endDate: Date exception?: boolean - changed: boolean + originalEndDate: Date + originalException: boolean } const ScrollWrapper = styled.div` @@ -44,17 +46,24 @@ const ScrollWrapper = styled.div` height: 40vh; ` +type ClubApplicationWithClub = ClubApplication & { + club__name: string + club__code: number +} + const WhartonApplicationCycles = (): ReactElement => { const [editMembership, setEditMembership] = useState(false) const [membershipCycle, setMembershipCycle] = useState<Cycle>({ name: '', id: null, + endDate: new Date(), }) const [editExtensions, setEditExtensions] = useState(false) const [extensionsCycle, setExtensionsCycle] = useState<Cycle>({ name: '', id: null, + endDate: new Date(), }) const [clubsSelectedMembership, setClubsSelectedMembership] = useState< @@ -81,7 +90,10 @@ const WhartonApplicationCycles = (): ReactElement => { const closeExtensionsModal = (): void => { setEditExtensions(false) // calculate clubs that have changed - const clubsToUpdate = clubsExtensions.filter((x) => x.changed) + const clubsToUpdate = clubsExtensions.filter( + (x) => + x.originalEndDate !== x.endDate || x.originalException !== x.exception, + ) // split into clubs with exceptions and clubs without const clubsExceptions = clubsToUpdate.filter((x) => x.exception) const clubsNoExceptions = clubsToUpdate.filter((x) => !x.exception) @@ -147,18 +159,23 @@ const WhartonApplicationCycles = (): ReactElement => { useEffect(() => { if (extensionsCycle && extensionsCycle.id != null) { - doApiRequest(`/cycles/${extensionsCycle.id}/clubs?format=json`) + doApiRequest( + `/cycles/${extensionsCycle.id}/club_applications?format=json`, + ) .then((resp) => resp.json()) .then((data) => { - const initialOptions = data.map((club: ClubApplication) => { - return { - id: club.id, - clubName: club.name, - endDate: new Date(club.application_end_time), - exception: club.application_end_time_exception, - changed: false, - } - }) + const initialOptions = data.map( + (application: ClubApplicationWithClub) => { + return { + id: application.id, + clubName: application.club__name, + endDate: new Date(application.application_end_time), + exception: application.application_end_time_exception, + originalEndDate: new Date(application.application_end_time), + originalException: application.application_end_time_exception, + } + }, + ) setClubsExtensions(initialOptions) }) } @@ -190,7 +207,11 @@ const WhartonApplicationCycles = (): ReactElement => { <button className="button is-info is-small" onClick={() => { - setMembershipCycle({ name: object.name, id: object.id }) + setMembershipCycle({ + name: object.name, + id: object.id, + endDate: new Date(object.end_date), + }) setEditMembership(true) setEditExtensions(false) }} @@ -200,7 +221,11 @@ const WhartonApplicationCycles = (): ReactElement => { <button className="button is-info is-small" onClick={() => { - setExtensionsCycle({ name: object.name, id: object.id }) + setExtensionsCycle({ + name: object.name, + id: object.id, + endDate: new Date(object.end_date), + }) setEditExtensions(true) setEditMembership(false) }} @@ -290,7 +315,6 @@ const WhartonApplicationCycles = (): ReactElement => { selected={club.endDate} onChange={(date) => { club.endDate = date - club.changed = true setClubsExtensions([...clubsExtensions]) }} /> @@ -299,7 +323,6 @@ const WhartonApplicationCycles = (): ReactElement => { <Checkbox onChange={(e) => { club.exception = e.target.checked - club.changed = true setClubsExtensions([...clubsExtensions]) }} checked={ @@ -320,9 +343,27 @@ const WhartonApplicationCycles = (): ReactElement => { className="button is-primary" style={{ position: 'absolute', bottom: 10, right: 10 }} onClick={closeExtensionsModal} + disabled={clubsExtensions.some( + (x) => + // For the case where we change end date without giving an exception to a club without one + !x.exception && + !x.originalException && + x.endDate.getTime() !== extensionsCycle.endDate.getTime(), + )} > Submit </button> + {clubsExtensions.some( + (x) => + !x.exception && + !x.originalException && + x.endDate.getTime() !== extensionsCycle.endDate.getTime(), + ) && ( + <p className="is-danger"> + To change the end date for a club, you must also check its + exception box. + </p> + )} </> )} </Modal>