diff --git a/src/App/routes/index.tsx b/src/App/routes/index.tsx index f51e8ee..e8d8987 100644 --- a/src/App/routes/index.tsx +++ b/src/App/routes/index.tsx @@ -127,6 +127,19 @@ const preferences = customWrapRoute({ }, }); +const historicalAlerts = customWrapRoute({ + parent: rootLayout, + path: 'historical-alerts', + component: { + render: () => import('#views/HistoricalAlerts'), + props: {}, + }, + context: { + title: 'Historical Alerts', + visibility: 'anything', + }, +}); + const about = customWrapRoute({ parent: rootLayout, path: 'about', @@ -261,6 +274,7 @@ const wrappedRoutes = { resendValidationEmail, mySubscription, cookiePolicy, + historicalAlerts, }; export const unwrappedRoutes = unwrapRoute(Object.values(wrappedRoutes)); diff --git a/src/components/Navbar/i18n.json b/src/components/Navbar/i18n.json index 9d2e622..7ffcd23 100644 --- a/src/components/Navbar/i18n.json +++ b/src/components/Navbar/i18n.json @@ -6,6 +6,7 @@ "appAbout": "About", "appResources": "Resources", "headerMenuHome": "Home", - "headerMenuMySubscription": "My Subscriptions" + "headerMenuMySubscription": "My Subscriptions", + "historicalAlerts": "Historical Alerts" } } diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index de2821f..eb1ebc2 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -79,11 +79,17 @@ function Navbar(props: Props) { > {strings.headerMenuHome} + {strings.headerMenuMySubscription} + + {strings.historicalAlerts} + diff --git a/src/views/HistoricalAlerts/AlertActions/i18n.json b/src/views/HistoricalAlerts/AlertActions/i18n.json new file mode 100644 index 0000000..28dcdf3 --- /dev/null +++ b/src/views/HistoricalAlerts/AlertActions/i18n.json @@ -0,0 +1,6 @@ +{ + "namespace": "alertActions", + "strings": { + "alertTableViewDetailsTitle": "View Details" + } +} \ No newline at end of file diff --git a/src/views/HistoricalAlerts/AlertActions/index.tsx b/src/views/HistoricalAlerts/AlertActions/index.tsx new file mode 100644 index 0000000..aa20e83 --- /dev/null +++ b/src/views/HistoricalAlerts/AlertActions/index.tsx @@ -0,0 +1,56 @@ +import { useCallback } from 'react'; +import { generatePath } from 'react-router-dom'; +import { CopyLineIcon } from '@ifrc-go/icons'; +import { Button } from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; + +import Link from '#components/Link'; +import { AlertInformationsQuery } from '#generated/types/graphql'; +import useAlert from '#hooks/useAlert'; +import routes from '#routes'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +type AlertType = NonNullable['alerts']>['items']>[number]; + +export interface Props { + data: AlertType; +} +function AlertActions(props: Props) { + const { data } = props; + const strings = useTranslation(i18n); + const alert = useAlert(); + + const url = generatePath( + routes.alertDetails.absolutePath, + { alertId: data.id }, + ); + + const handleClick = useCallback(() => { + navigator.clipboard.writeText(`${window.location.origin}${url}`); + alert.show('Link copied to clipboard'); + }, [url, alert]); + + return ( +
+ + {strings.alertTableViewDetailsTitle} + + +
+ ); +} + +export default AlertActions; diff --git a/src/views/HistoricalAlerts/AlertActions/styles.module.css b/src/views/HistoricalAlerts/AlertActions/styles.module.css new file mode 100644 index 0000000..39705f6 --- /dev/null +++ b/src/views/HistoricalAlerts/AlertActions/styles.module.css @@ -0,0 +1,6 @@ +.alert-actions{ + display: flex; + gap: var(--go-ui-spacing-sm); +} + + diff --git a/src/views/HistoricalAlerts/i18n.json b/src/views/HistoricalAlerts/i18n.json new file mode 100644 index 0000000..a1027bf --- /dev/null +++ b/src/views/HistoricalAlerts/i18n.json @@ -0,0 +1,33 @@ +{ + "namespace": "historicalAlerts", + "strings": { + "allOngoingAlertTitle":"Past 3 Months Alerts ({numAppeals}) ", + "historicalAlertTableEventTitle":"Event" , + "historicalAlertTableCategoryTitle":"Event Categories", + "historicalAlertTableRegionTitle":"Region", + "historicalAlertTableCountryTitle":"Country", + "historicalAlertTableActionsTitle":"Actions", + "historicalAlertTableAdminsTitle":"Admin1s", + "historicalAlertTableSentLabel":"Sent", + "tableViewAllSources": "View All Sources", + "historicalAlertTitle": "IFRC Alert Hub - Historical Alerts", + "historicalAlert": "Historical Alerts", + "filterCountriesPlaceholder": "All Countries", + "filterAdmin1Placeholder": "All Admin1", + "filterUrgencyPlaceholder": "All Urgency Types", + "filterSeverityPlaceholder": "All Severity Types", + "filterCertaintyPlaceholder": "All Certainty Types", + "filterCountriesLabel": "Country", + "filterAdmin1Label": "Admin1", + "filterUrgencyLabel": "Urgency Level", + "filterSeverityLabel": "Severity Level", + "filterCertaintyLabel": "Certainty Level", + "filterRegionsLabel": "Regions", + "filterRegionsPlaceholder": "All Regions", + "filterCategoriesLabel": "Event Categories", + "filterCategoriesPlaceholder": "All Event Categories", + "filterStartDateFrom":"Start date from", + "filterStartDateTo":"Start date To", + "historicalAlertDescription": "IFRC Alert Hub provides global emergency alerts, empowering communities to protect lives and livelihoods. Easily access and filter past alerts from the latest months to stay informed." + } +} diff --git a/src/views/HistoricalAlerts/index.tsx b/src/views/HistoricalAlerts/index.tsx new file mode 100644 index 0000000..32dad1f --- /dev/null +++ b/src/views/HistoricalAlerts/index.tsx @@ -0,0 +1,495 @@ +import { + ComponentType, + HTMLProps, + useCallback, + useMemo, +} from 'react'; +import { + gql, + useQuery, +} from '@apollo/client'; +import { ChevronRightLineIcon } from '@ifrc-go/icons'; +import { + Container, + DateInput, + DateOutput, + DateOutputProps, + MultiSelectInput, + Pager, + SelectInput, + Table, +} from '@ifrc-go/ui'; +import { SortContext } from '@ifrc-go/ui/contexts'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + createElementColumn, + createListDisplayColumn, + createStringColumn, + resolveToString, +} from '@ifrc-go/ui/utils'; +import { + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; + +import Link from '#components/Link'; +import Page from '#components/Page'; +import { + AlertEnumsAndAllCountryListQuery, + AlertEnumsAndAllCountryListQueryVariables, + AlertEnumsQuery, + AlertFilter, + AlertInformationsQuery, + AlertInformationsQueryVariables, + FilteredAdminListQuery, + FilteredAdminListQueryVariables, + OffsetPaginationInput, +} from '#generated/types/graphql'; +import useFilterState from '#hooks/useFilterState'; +import { DATE_FORMAT } from '#utils/constants'; +import { + stringIdSelector, + stringNameSelector, +} from '#utils/selectors'; +import AlertFilters from '#views/Home/AlertFilters'; + +import AlertActions, { type Props as AlertActionsProps } from './AlertActions'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +// TODO: Add Historical alert query here + +const ALERT_INFORMATIONS = gql` + query AlertInformations( + $order:AlertOrder, + $pagination: OffsetPaginationInput, + $filters: AlertFilter, + ) { + public { + id + alerts( + pagination: $pagination, + filters: $filters, + order:$order, + ) { + limit + offset + count + items { + id + country { + id + name + region { + id + name + } + } + admin1s { + id + name + } + sent + info { + id + event + alertId + categoryDisplay + } + } + } + } + } +`; + +const ALERT_ENUMS_AND_ALL_COUNTRY = gql` +query AlertEnumsAndAllCountryList { + enums { + AlertInfoCertainty { + key + label + } + AlertInfoUrgency { + key + label + } + AlertInfoSeverity { + key + label + } + AlertInfoCategory { + key + label + } + } + public { + id + allCountries { + name + id + } + } +} +`; + +const ADMIN_LIST = gql` +query FilteredAdminList($filters:Admin1Filter, $pagination: OffsetPaginationInput) { + public { + id + admin1s(filters: $filters, pagination: $pagination) { + items { + id + name + countryId + alertCount + } + } + } +} +`; + +type AdminOption = NonNullable['admin1s']>['items']>[number]; + +type Urgency = NonNullable[number]; +type Severity = NonNullable[number]; +type Certainty = NonNullable[number]; +type Category = NonNullable[number]; + +type AlertType = NonNullable['alerts']>['items']>[number]; +type Admin1 = AlertType['admin1s'][number]; + +const adminKeySelector = (admin1: AdminOption) => admin1.id; +const urgencyKeySelector = (urgency: Urgency) => urgency.key; +const severityKeySelector = (severity: Severity) => severity.key; +const certaintyKeySelector = (certainty: Certainty) => certainty.key; +const labelSelector = (alert: AlertFilters) => alert.label; +const categoryKeySelector = (category: Category) => category.key; + +const alertKeySelector = (item: AlertType) => item.id; +const PAGE_SIZE = 20; +const ASC = 'ASC'; +const DESC = 'DESC'; + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const strings = useTranslation(i18n); + + const { + sortState, + limit, + page, + rawFilter, + setPage, + filter, + setFilterField, + filtered, + offset, + } = useFilterState({ + pageSize: PAGE_SIZE, + filter: {}, + }); + + const order = useMemo(() => { + if (isNotDefined(sortState.sorting)) { + return undefined; + } + return { + [sortState.sorting.name]: sortState.sorting.direction === 'asc' ? ASC : DESC, + }; + }, [sortState.sorting]); + + const variables = useMemo<{ filters: AlertFilter, pagination: OffsetPaginationInput }>(() => ({ + pagination: { + offset, + limit, + }, + order, + filters: { + urgency: filter.urgency, + severity: filter.severity, + certainty: filter.certainty, + category: filter.category, + country: isDefined(filter.country?.pk) ? { pk: filter.country.pk } : undefined, + admin1: filter.admin1, + sent: isDefined(filter.sent) ? { + // TODO: Add start date & end date + range: { + end: filter.sent, + start: filter.sent, + }, + } : undefined, + }, + }), [ + order, + limit, + offset, + filter, + ]); + + const { + loading: alertInfoLoading, + previousData, + data: alertInfosResponse = previousData, + error: alertInfoError, + } = useQuery( + ALERT_INFORMATIONS, + { + skip: isNotDefined(variables), + variables, + }, + ); + + const { + data: alertEnumsResponse, + } = useQuery( + ALERT_ENUMS_AND_ALL_COUNTRY, + ); + + const adminQueryVariables = useMemo( + () => { + if (isNotDefined(filter.country)) { + return { + filters: undefined, + // FIXME: Implement search select input + pagination: { + offset: 0, + limit: 500, + }, + }; + } + + return { + filters: { + country: { pk: filter.country.pk }, + }, + // FIXME: Implement search select input + pagination: { + offset: 0, + limit: 500, + }, + }; + }, + [filter.country], + ); + + const { + data: adminResponse, + } = useQuery( + ADMIN_LIST, + { variables: adminQueryVariables, skip: isNotDefined(filter.country) }, + ); + + const data = alertInfosResponse?.public.alerts; + + const columns = useMemo( + () => ([ + createStringColumn( + 'event', + strings.historicalAlertTableEventTitle, + (item) => item.info?.event, + { columnClassName: styles.event }, + ), + createStringColumn( + 'category', + strings.historicalAlertTableCategoryTitle, + (item) => item.info?.categoryDisplay, + { columnClassName: styles.category }, + ), + createStringColumn( + 'region', + strings.historicalAlertTableRegionTitle, + (item) => (item.country.region.name), + { columnClassName: styles.region }, + + ), + createStringColumn( + 'country', + strings.historicalAlertTableCountryTitle, + (item) => (item.country.name), + { columnClassName: styles.country }, + ), + createListDisplayColumn>( + 'admin1s', + strings.historicalAlertTableAdminsTitle, + (item) => ({ + list: item.admin1s, + keySelector: ({ id }) => id, + renderer: 'span' as unknown as ComponentType>, + rendererParams: ({ name }) => ({ children: name }), + }), + { columnClassName: styles.admins }, + ), + createElementColumn( + 'sent', + strings.historicalAlertTableSentLabel, + DateOutput, + (_, item) => ({ + value: item.sent, + format: DATE_FORMAT, + }), + { + sortable: true, + columnClassName: styles.sent, + }, + ), + createElementColumn( + 'actions', + strings.historicalAlertTableActionsTitle, + AlertActions, + (_, item) => ({ data: item }), + { + columnClassName: styles.actions, + cellRendererClassName: styles.actions, + }, + ), + ]), + [ + strings.historicalAlertTableEventTitle, + strings.historicalAlertTableCategoryTitle, + strings.historicalAlertTableRegionTitle, + strings.historicalAlertTableCountryTitle, + strings.historicalAlertTableAdminsTitle, + strings.historicalAlertTableSentLabel, + strings.historicalAlertTableActionsTitle, + ], + ); + const heading = resolveToString( + strings.allOngoingAlertTitle, + { numAppeals: data?.count ?? '--' }, + ); + + const handleCountryFilterChange = useCallback((countryId: string | undefined) => { + setFilterField(countryId ? { pk: countryId } : undefined, 'country'); + }, [setFilterField]); + + return ( + + + )} + > + {strings.tableViewAllSources} + + )} + overlayPending + pending={alertInfoLoading} + errored={isDefined(alertInfoError)} + errorMessage={alertInfoError?.message} + footerActions={isDefined(data) && ( + + )} + filters={( + <> + + + + + {/* // TODO Add start date and end date filter */} + { }} + /> + { }} + /> + + + + )} + > + + + + + + ); +} + +Component.displayName = 'HistoricalAlerts'; diff --git a/src/views/HistoricalAlerts/styles.module.css b/src/views/HistoricalAlerts/styles.module.css new file mode 100644 index 0000000..2b1da9d --- /dev/null +++ b/src/views/HistoricalAlerts/styles.module.css @@ -0,0 +1,68 @@ +.historical-alerts { + .alerts-table { + overflow: auto; + + .alert-info { + display: flex; + align-items: flex-end; + + .alert-icon { + font-size: var(--go-ui-font-size-md); + } + } + + .main-content { + flex-grow: 1; + overflow: auto; + } + + .event { + width: 0%; + min-width: 8rem; + } + + .category { + width: 0%; + min-width: 5rem; + } + + .region { + width: 0%; + min-width: 7rem; + } + + .country { + width: 0%; + min-width: 8rem; + } + + .admins { + min-width: 14rem; + } + + .sent { + width: 0; + min-width: 7rem; + } + + .actions { + width: 0; + min-width: 10rem; + color: var(--go-ui-color-text); + font-weight: var(--go-ui-font-weight-medium); + } + + .sources { + display: flex; + align-items: center; + text-decoration: none; + color: var(--go-ui-color-text); + font-weight: var(--go-ui-font-weight-medium); + } + + .sources:hover { + text-decoration: underline; + color: var(--go-ui-color-primary-red); + } + } +} \ No newline at end of file diff --git a/src/views/MySubscription/index.tsx b/src/views/MySubscription/index.tsx index 96131d9..1bab16b 100644 --- a/src/views/MySubscription/index.tsx +++ b/src/views/MySubscription/index.tsx @@ -113,12 +113,11 @@ export function Component() { )} > - {showSubscriptionModal && data?.map((subscription) => ( + {showSubscriptionModal && ( - ))} + )} void; }