diff --git a/src/components/CustomTheme.js b/src/components/CustomTheme.js index 7e8581ce0..5787af69a 100644 --- a/src/components/CustomTheme.js +++ b/src/components/CustomTheme.js @@ -11,7 +11,8 @@ const theme = createTheme({ contrast: "#FFFFFF" }, background: { - light: "#F7F7F9" + light: "#F7F7F9", + light_gray: "#eaeaea" }, text: { primary: "#000000DE", @@ -34,10 +35,10 @@ const theme = createTheme({ fontSize: "12px", fontWeight: 400 }, - subtitle2: ({ theme }) => ({ + subtitle2: ({ theme: t }) => ({ fontSize: "14px", fontWeight: 500, - color: theme.palette.text.primary + color: t.palette.text.primary }), h4: { fontSize: "34px", @@ -126,8 +127,8 @@ const theme = createTheme({ root: { fontSize: "12px" }, - standardInfo: ({ theme }) => ({ - color: theme.palette.primary.dark + standardInfo: ({ theme: t }) => ({ + color: t.palette.primary.dark }) } } diff --git a/src/i18n/en.json b/src/i18n/en.json index 0bb5e83e8..a685042bf 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -353,6 +353,14 @@ "refund_amount_emitted": "Amount Refund", "ticket_types": "Active Tickets per Ticket Types", "badge_types": "Active Tickets per Badge Types", + "general_dates": "General Dates", + "ordering": "Ordering", + "important_documents": "Important Documents", + "sponsor_levels": "Sponsor Levels", + "pages": "Pages", + "tab_badge_types": "Badge Types", + "media_uploads": "Media Uploads", + "booth_layout_types": "Booth Layout Types", "expand": "Expand section", "collapse": "Collapse section", "badge_features_tickets": "Active Tickets per Badge Features", diff --git a/src/pages/summits/components/dashboard-section.js b/src/pages/summits/components/dashboard-section.js new file mode 100644 index 000000000..dd2a814c9 --- /dev/null +++ b/src/pages/summits/components/dashboard-section.js @@ -0,0 +1,40 @@ +import React from "react"; +import PropTypes from "prop-types"; +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import CardHeader from "@mui/material/CardHeader"; +import Divider from "@mui/material/Divider"; +import Typography from "@mui/material/Typography"; + +function DashboardSection({ title, children, variant }) { + if (variant === "card") { + return ( + + + + {children} + + ); + } + + return ( + + + {title} + + {children} + + ); +} + +DashboardSection.propTypes = { + title: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, + variant: PropTypes.oneOf(["card"]) +}; + +DashboardSection.defaultProps = { + variant: undefined +}; + +export default DashboardSection; diff --git a/src/pages/summits/components/dashboard-stat-section.js b/src/pages/summits/components/dashboard-stat-section.js new file mode 100644 index 000000000..2aecf6853 --- /dev/null +++ b/src/pages/summits/components/dashboard-stat-section.js @@ -0,0 +1,46 @@ +import React from "react"; +import PropTypes from "prop-types"; +import Divider from "@mui/material/Divider"; +import Grid2 from "@mui/material/Grid2"; +import DashboardSection from "./dashboard-section"; +import SummitDashboardStat from "./summit-dashboard-stat"; + +const GRID_COLUMNS = 12; + +function DashboardStatSection({ title, rows }) { + return ( + + {rows.map((group, i) => { + if (!group.length) return null; + const size = Math.floor(GRID_COLUMNS / group.length); + const key = group[0]?.title ?? `group-${i}`; + return ( + + {i > 0 && } + + {group.map(({ title: label, stat: value }) => ( + + + + ))} + + + ); + })} + + ); +} + +DashboardStatSection.propTypes = { + title: PropTypes.string.isRequired, + rows: PropTypes.arrayOf( + PropTypes.arrayOf( + PropTypes.shape({ + title: PropTypes.string.isRequired, + stat: PropTypes.number + }) + ) + ).isRequired +}; + +export default DashboardStatSection; diff --git a/src/pages/summits/components/summit-dashboard-date-range.js b/src/pages/summits/components/summit-dashboard-date-range.js new file mode 100644 index 000000000..d0141010c --- /dev/null +++ b/src/pages/summits/components/summit-dashboard-date-range.js @@ -0,0 +1,50 @@ +import React from "react"; +import PropTypes from "prop-types"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import RemoveIcon from "@mui/icons-material/Remove"; +import { formatDate } from "../../../utils/methods"; +import { DATETIME_FORMAT } from "../../../utils/constants"; + +function SummitDashboardDateRange({ label, startTs, endTs, tzName }) { + if (!startTs || !endTs) return null; + + return ( + + + {label} + + + {formatDate(startTs, tzName, DATETIME_FORMAT)} + + + + {formatDate(endTs, tzName, DATETIME_FORMAT)} + + + ); +} + +SummitDashboardDateRange.propTypes = { + label: PropTypes.string.isRequired, + startTs: PropTypes.number, + endTs: PropTypes.number, + tzName: PropTypes.string +}; + +SummitDashboardDateRange.defaultProps = { + startTs: null, + endTs: null, + tzName: undefined +}; + +export default SummitDashboardDateRange; diff --git a/src/pages/summits/components/summit-dashboard-stat.js b/src/pages/summits/components/summit-dashboard-stat.js new file mode 100644 index 000000000..687b23fbc --- /dev/null +++ b/src/pages/summits/components/summit-dashboard-stat.js @@ -0,0 +1,26 @@ +import React from "react"; +import PropTypes from "prop-types"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; + +function SummitDashboardStat({ label, value }) { + return ( + + + {label} + + {value} + + ); +} + +SummitDashboardStat.propTypes = { + label: PropTypes.string.isRequired, + value: PropTypes.number +}; + +SummitDashboardStat.defaultProps = { + value: 0 +}; + +export default SummitDashboardStat; diff --git a/src/pages/summits/summit-dashboard-page.js b/src/pages/summits/summit-dashboard-page.js index 010bc48d0..72817c6c1 100644 --- a/src/pages/summits/summit-dashboard-page.js +++ b/src/pages/summits/summit-dashboard-page.js @@ -1,5 +1,5 @@ /** - * Copyright 2017 OpenStack Foundation + * Copyright 2026 OpenStack Foundation * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -10,430 +10,201 @@ * See the License for the specific language governing permissions and * limitations under the License. * */ -import React from "react"; +import React, { useEffect } from "react"; import PropTypes from "prop-types"; import { connect } from "react-redux"; -import moment from "moment-timezone"; import T from "i18n-react/dist/i18n-react"; import { Breadcrumb } from "react-breadcrumbs"; +import Box from "@mui/material/Box"; +import Container from "@mui/material/Container"; +import Grid2 from "@mui/material/Grid2"; +import Stack from "@mui/material/Stack"; +import Tab from "@mui/material/Tab"; +import Tabs from "@mui/material/Tabs"; +import Typography from "@mui/material/Typography"; +import DashboardStatSection from "./components/dashboard-stat-section"; import { getSummitById } from "../../actions/summit-actions"; +import { getRegistrationData } from "../../actions/summit-stats-actions"; import Member from "../../models/member"; -import "../../styles/summit-dashboard-page.less"; +import SummitDashboardDateRange from "./components/summit-dashboard-date-range"; +import DashboardSection from "./components/dashboard-section"; -class SummitDashboardPage extends React.Component { - constructor(props) { - super(props); +const TAB_KEYS = [ + "dashboard.dashboard", + "dashboard.ordering", + "dashboard.important_documents", + "dashboard.sponsor_levels", + "dashboard.pages", + "dashboard.tab_badge_types", + "dashboard.media_uploads", + "dashboard.booth_layout_types" +]; - this.interval = null; +function SummitDashboardPage({ + currentSummit, + member, + match, + totalOrders, + totalActiveTickets, + getRegistrationData: fetchRegistrationData +}) { + useEffect(() => { + fetchRegistrationData(); + }, [currentSummit.id]); - this.state = { - localtime: moment(), - collapseState: { - emails: true, - events: true, - voting: true - } - }; - this.onCollapseChange = this.onCollapseChange.bind(this); - } + const canEditSummit = new Member(member).canEditSummit(); + const tz = currentSummit.time_zone?.name; + const venueCount = currentSummit.locations.filter( + (l) => l.class_name === "SummitVenue" + ).length; - onCollapseChange(section) { - const newCollapseState = { ...this.state.collapseState }; - newCollapseState[section] = !newCollapseState[section]; - this.setState({ ...this.state, collapseState: newCollapseState }); - } + return ( + + - componentDidMount() { - const { currentSummit } = this.props; - this.interval = setInterval( - this.localTimer.bind(this), - moment.duration(1, "second").asMilliseconds() - ); + + {currentSummit.name} + - if (currentSummit?.time_zone?.name) { - const localtime = moment().tz(currentSummit.time_zone.name); - this.setState({ ...this.state, localtime }); - } - } + + {}}> + {TAB_KEYS.map((key, i) => ( + + ))} + + - componentWillUnmount() { - clearInterval(this.interval); - } - - localTimer() { - this.setState({ - localtime: this.state.localtime.add(1, "second") - }); - } - - getFormattedTime(atime) { - return moment - .unix(atime) - .tz(this.props.currentSummit.time_zone.name) - .format("MMMM Do YYYY, h:mm:ss a"); - } - - getTimeClass(start_time, end_time) { - if (this.state.localtime.isBefore(moment(start_time))) return "future"; - if (this.state.localtime.isAfter(moment(end_time))) return "past"; - return "present"; - } - - render() { - const { currentSummit, match, member } = this.props; - const memberObj = new Member(member); - const canEditSummit = memberObj.canEditSummit(); + + + + + + + + {currentSummit.selection_plans.map((sp) => ( + + + + + + ))} + + - if (!currentSummit.id || !currentSummit.time_zone?.name) return
; + {canEditSummit && ( + + + - return ( -
- -
-

- {currentSummit.name} {T.translate("general.summit")} -

-
-

{T.translate("dashboard.dates")}

-
-
{currentSummit.time_zone.name}
-
- {" "} - {this.getFormattedTime(this.state.localtime.unix())}{" "} -
-
-
-
- {" "} - {T.translate("general.summit")}{" "} -
-
- {" "} - {this.getFormattedTime(currentSummit.start_date)}{" "} -
-
- {" "} - {" "} -
-
- {" "} - {this.getFormattedTime(currentSummit.end_date)}{" "} -
-
-
-
- {" "} - {" "} - {T.translate("dashboard.registration")}{" "} -
-
- {" "} - {this.getFormattedTime( - currentSummit.registration_begin_date - )}{" "} -
-
- {" "} - {" "} -
-
- {" "} - {this.getFormattedTime(currentSummit.registration_end_date)}{" "} -
-
- {canEditSummit && - currentSummit.selection_plans.map((sp) => ( -
-
{sp.name}
-
-
-
- {" "} - {" "} - {T.translate("dashboard.submission")}{" "} -
-
- {" "} - {this.getFormattedTime(sp.submission_begin_date)}{" "} -
-
- {" "} - {" "} -
-
- {" "} - {this.getFormattedTime(sp.submission_end_date)}{" "} -
-
-
+ + -
- {" "} - {" "} - {T.translate("dashboard.voting")}{" "} -
-
- {" "} - {this.getFormattedTime(sp.voting_begin_date)}{" "} -
-
- {" "} - {" "} -
-
- {" "} - {this.getFormattedTime(sp.voting_end_date)}{" "} -
-
-
-
- {" "} - {" "} - {T.translate("dashboard.selection")}{" "} -
-
- {" "} - {this.getFormattedTime(sp.selection_begin_date)}{" "} -
-
- {" "} - {" "} -
-
- {" "} - {this.getFormattedTime(sp.selection_end_date)}{" "} -
-
-
-
- ))} - - {canEditSummit && ( -
-
-

- {T.translate("dashboard.events")}  - {this.state.collapseState.events && ( - this.onCollapseChange("events")} - className="fa fa-plus-square clickable" - aria-hidden="true" - /> - )} - {!this.state.collapseState.events && ( - this.onCollapseChange("events")} - className="fa fa-minus-square clickable" - aria-hidden="true" - /> - )} -

- {!this.state.collapseState.events && ( -
-
-
- -  {T.translate("general.speakers")}  - {currentSummit.speakers_count} -
-
- -  {T.translate("dashboard.submitted_events")}  - - {currentSummit.presentations_submitted_count} - -
-
- -  {T.translate("dashboard.published_events")}  - {currentSummit.published_events_count} -
-
-
-
- -  {T.translate("dashboard.venues")}  - - { - currentSummit.locations.filter( - (l) => l.class_name === "SummitVenue" - ).length - } - -
-
-
- )} -
-

- {T.translate("dashboard.voting")}  - {this.state.collapseState.voting && ( - this.onCollapseChange("voting")} - className="fa fa-plus-square clickable" - aria-hidden="true" - /> - )} - {!this.state.collapseState.voting && ( - this.onCollapseChange("voting")} - className="fa fa-minus-square clickable" - aria-hidden="true" - /> - )} -

- {!this.state.collapseState.voting && ( -
-
-
- -  {T.translate("dashboard.voters")}  - {currentSummit.presentation_voters_count} -
-
- -  {T.translate("dashboard.votes")}  - {currentSummit.presentation_votes_count} -
-
-
- )} -
-

- {T.translate("dashboard.emails")}  - {this.state.collapseState.emails && ( - this.onCollapseChange("emails")} - className="fa fa-plus-square clickable" - aria-hidden="true" - /> - )} - {!this.state.collapseState.emails && ( - this.onCollapseChange("emails")} - className="fa fa-minus-square clickable" - aria-hidden="true" - /> - )} -

- {!this.state.collapseState.emails && ( -
-
-
- -  {T.translate("dashboard.accepted")}  - - { - currentSummit.speaker_announcement_email_accepted_count - } - -
-
- -  {T.translate("dashboard.rejected")}  - - { - currentSummit.speaker_announcement_email_rejected_count - } - -
-
- -  {T.translate("dashboard.alternate")}  - - { - currentSummit.speaker_announcement_email_alternate_count - } - -
-
-
-
- -  {T.translate("dashboard.accepted_alternate")}  - - { - currentSummit.speaker_announcement_email_accepted_alternate_count - } - -
-
- -  {T.translate("dashboard.accepted_rejected")}  - - { - currentSummit.speaker_announcement_email_accepted_rejected_count - } - -
-
- -  {T.translate("dashboard.alternate_rejected")}  - - { - currentSummit.speaker_announcement_email_alternate_rejected_count - } - -
-
-
- )} -
- )} -
-
- ); - } + ] + ]} + /> +
+
+ )} + + + ); } SummitDashboardPage.propTypes = { @@ -447,13 +218,11 @@ SummitDashboardPage.propTypes = { end_date: PropTypes.number, registration_begin_date: PropTypes.number, registration_end_date: PropTypes.number, - selection_plans: PropTypes.arrayOf(PropTypes.object), - locations: PropTypes.arrayOf(PropTypes.object), + selection_plans: PropTypes.arrayOf(PropTypes.shape({})), + locations: PropTypes.arrayOf(PropTypes.shape({})), speakers_count: PropTypes.number, presentations_submitted_count: PropTypes.number, published_events_count: PropTypes.number, - presentation_voters_count: PropTypes.number, - presentation_votes_count: PropTypes.number, speaker_announcement_email_accepted_count: PropTypes.number, speaker_announcement_email_rejected_count: PropTypes.number, speaker_announcement_email_alternate_count: PropTypes.number, @@ -461,17 +230,33 @@ SummitDashboardPage.propTypes = { speaker_announcement_email_accepted_rejected_count: PropTypes.number, speaker_announcement_email_alternate_rejected_count: PropTypes.number }).isRequired, - member: PropTypes.object, + member: PropTypes.shape({}), match: PropTypes.shape({ url: PropTypes.string - }).isRequired + }).isRequired, + totalOrders: PropTypes.number, + totalActiveTickets: PropTypes.number, + getRegistrationData: PropTypes.func.isRequired +}; + +SummitDashboardPage.defaultProps = { + member: null, + totalOrders: 0, + totalActiveTickets: 0 }; -const mapStateToProps = ({ currentSummitState, loggedUserState }) => ({ +const mapStateToProps = ({ + currentSummitState, + loggedUserState, + summitStatsState +}) => ({ currentSummit: currentSummitState.currentSummit, - member: loggedUserState.member + member: loggedUserState.member, + totalOrders: summitStatsState.total_orders, + totalActiveTickets: summitStatsState.total_active_tickets }); export default connect(mapStateToProps, { - getSummitById + getSummitById, + getRegistrationData })(SummitDashboardPage); diff --git a/src/styles/summit-dashboard-page.less b/src/styles/summit-dashboard-page.less deleted file mode 100644 index a6afd9f96..000000000 --- a/src/styles/summit-dashboard-page.less +++ /dev/null @@ -1,18 +0,0 @@ -.dashboard { - .row { - font-size: 20px; - margin: 20px 0; - } - .current { - color: #00ca00; - } - .past { - color: red; - } - .future { - color: black; - } - i.clickable { - cursor: pointer; - } -}