From ba28d86990ead5deedc223e38895310e15459514 Mon Sep 17 00:00:00 2001 From: roshni73 Date: Fri, 18 Oct 2024 16:50:39 +0545 Subject: [PATCH 01/30] Add login page - Add Captcha for login page - Fix Hacptcha sitekey --- backend | 2 +- env.ts | 3 +- package.json | 2 + src/App/routes/index.tsx | 14 +++ src/components/Captcha/index.tsx | 85 ++++++++++++++++ src/components/Navbar/index.tsx | 8 +- src/config.ts | 2 + src/views/Login/i18n.json | 22 +++++ src/views/Login/index.tsx | 155 ++++++++++++++++++++++++++++++ src/views/Login/styles.module.css | 39 ++++++++ vite.config.ts | 3 +- 11 files changed, 327 insertions(+), 8 deletions(-) create mode 100644 src/components/Captcha/index.tsx create mode 100644 src/views/Login/i18n.json create mode 100644 src/views/Login/index.tsx create mode 100644 src/views/Login/styles.module.css diff --git a/backend b/backend index 69450f82..c02b63c4 160000 --- a/backend +++ b/backend @@ -1 +1 @@ -Subproject commit 69450f82253f675cf923779396f46cb2e809dcf8 +Subproject commit c02b63c4c220bae7de86b88a1627ecca7d00d8bd diff --git a/env.ts b/env.ts index ed2f43ac..70782668 100644 --- a/env.ts +++ b/env.ts @@ -1,6 +1,5 @@ import { defineConfig, Schema } from '@julr/vite-plugin-validate-env'; -// TODO: Integrate .env for CI and remove optional() call on required fields export default defineConfig({ // Used in vite APP_GOOGLE_ANALYTICS_ID: Schema.string.optional(), @@ -23,6 +22,8 @@ export default defineConfig({ APP_GRAPHQL_API_ENDPOINT: Schema.string({ format: 'url', protocol: true, tld: false }), APP_MAPBOX_ACCESS_TOKEN: Schema.string(), + APP_HCAPTCHA_SITEKEY: Schema.string.optional(), + // Used in codegen APP_GRAPHQL_CODEGEN_ENDPOINT: Schema.string(), // NOTE: this is both url and file path diff --git a/package.json b/package.json index 235977c3..207e8f55 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@apollo/client": "^3.9.9", "@graphql-codegen/introspection": "^4.0.3", "@graphql-codegen/typescript-operations": "^4.2.0", + "@hcaptcha/react-hcaptcha": "^1.11.0", "@ifrc-go/icons": "^1.3.3", "@ifrc-go/ui": "^1.1.2", "@mapbox/mapbox-gl-draw": "^1.4.3", @@ -29,6 +30,7 @@ "@sentry/react": "^7.81.1", "@togglecorp/fujs": "^2.1.1", "@togglecorp/re-map": "^0.2.0-beta-6", + "@togglecorp/toggle-form": "^2.0.4", "@turf/bbox": "^7.1.0", "@turf/circle": "^7.1.0", "graphql": "^16.8.1", diff --git a/src/App/routes/index.tsx b/src/App/routes/index.tsx index c6c6b483..de89600d 100644 --- a/src/App/routes/index.tsx +++ b/src/App/routes/index.tsx @@ -178,6 +178,19 @@ const pageNotFound = customWrapRoute({ }, }); +const login = customWrapRoute({ + parent: rootLayout, + path: 'login', + component: { + render: () => import('#views/Login'), + props: {}, + }, + context: { + title: 'Login', + visibility: 'is-not-authenticated', + }, +}); + const wrappedRoutes = { rootLayout, homeLayout, @@ -190,6 +203,7 @@ const wrappedRoutes = { allSourcesFeeds, about, pageNotFound, + login, }; export const unwrappedRoutes = unwrapRoute(Object.values(wrappedRoutes)); diff --git a/src/components/Captcha/index.tsx b/src/components/Captcha/index.tsx new file mode 100644 index 00000000..f895d056 --- /dev/null +++ b/src/components/Captcha/index.tsx @@ -0,0 +1,85 @@ +import React, { useCallback } from 'react'; +import HCaptcha from '@hcaptcha/react-hcaptcha'; +import { + InputContainer, + InputContainerProps, +} from '@ifrc-go/ui'; + +import { hCaptchaKey } from '#config'; + +export type HCaptchaProps = Omit & { + name: T, + onChange: (value: string | undefined, name: T) => void; + elementRef?: React.RefObject; +}; + +function HCaptchaInput(props: HCaptchaProps) { + const { + actions, + actionsContainerClassName, + className, + disabled, + error, + errorContainerClassName, + hint, + hintContainerClassName, + icons, + iconsContainerClassName, + inputSectionClassName, + label, + readOnly, + name, + onChange, + elementRef, + } = props; + + const handleVerify = useCallback( + (token: string) => { + onChange(token, name); + }, + [onChange, name], + ); + const handleError = useCallback( + (err: string) => { + // eslint-disable-next-line no-console + console.error(err); + onChange(undefined, name); + }, + [onChange, name], + ); + const handleExpire = useCallback( + () => { + onChange(undefined, name); + }, + [onChange, name], + ); + + return ( + + )} + /> + ); +} + +export default HCaptchaInput; diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index e095a6af..f84881db 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -1,5 +1,4 @@ import { - Button, Heading, NavigationTabList, PageContainer, @@ -60,13 +59,12 @@ function Navbar(props: Props) { > {strings.appResources} - + ; +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType; + +const defaultFormValue: PartialFormFields = {}; + +const formSchema: FormSchema = { + fields: (): FormSchemaFields => ({ + email: { + required: true, + requiredValidation: requiredStringCondition, + }, + password: { + required: true, + requiredValidation: requiredStringCondition, + }, + captcha: { + required: true, + requiredValidation: requiredStringCondition, + }, + }), +}; + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const strings = useTranslation(i18n); + const { + value: formValue, + setFieldValue, + setError, + validate, + } = useForm(formSchema, { value: defaultFormValue }); + + const fieldError: PartialFormFields = {}; + + const handleFormSubmit = useMemo( + () => createSubmitHandler( + validate, + setError, + // FIXME: Add form submission logic here + () => {}, + ), + [validate, setError], + ); + + const signupInfo = resolveToComponent( + strings.loginDontHaveAccount, + { + signUpLink: ( + + {strings.loginSignUp} + + ), + }, + ); + + return ( + +
+
+ + +
+
+ + {strings.loginForgotUserPass} + + + {strings.loginResendValidation} + +
+
+ + +
+ {signupInfo} +
+
+
+
+ ); +} + +Component.displayName = 'Login'; diff --git a/src/views/Login/styles.module.css b/src/views/Login/styles.module.css new file mode 100644 index 00000000..f13ba8b8 --- /dev/null +++ b/src/views/Login/styles.module.css @@ -0,0 +1,39 @@ +.login { + .main-section { + display: flex; + justify-content: center; + + .form { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: var(--go-ui-spacing-xl); + max-width: var(--go-ui-width-content-max); + + .fields { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-xl); + } + + .utility-links { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-sm); + align-items: flex-end; + } + + .actions { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-lg); + align-items: center; + + .sign-up { + display: flex; + gap: var(--go-ui-spacing-sm); + } + } + } + } +} diff --git a/vite.config.ts b/vite.config.ts index 97d5b483..45403fd5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,6 +9,7 @@ import { ValidateEnv as validateEnv } from '@julr/vite-plugin-validate-env'; import { VitePluginRadar } from 'vite-plugin-radar'; import alertHubPackage from './package.json'; +import envConfig from './env'; /* Get commit hash */ const commitHash = execSync('git rev-parse --short HEAD').toString(); @@ -35,7 +36,7 @@ export default defineConfig(({ mode }) => { reactSwc(), tsconfigPaths(), webfontDownload(), - validateEnv(), + validateEnv(envConfig), isProd ? compression() : undefined, VitePluginRadar({ analytics: { From 4f99961af45c1498c15f1461af9c7d08cd4cdc6d Mon Sep 17 00:00:00 2001 From: roshni73 Date: Mon, 11 Nov 2024 16:10:25 +0545 Subject: [PATCH 02/30] Add Forget password page --- src/App/routes/index.tsx | 14 +++ src/views/Login/i18n.json | 2 +- src/views/Login/index.tsx | 2 +- src/views/RecoverAccount/i18n.json | 10 ++ src/views/RecoverAccount/index.tsx | 106 +++++++++++++++++++++ src/views/RecoverAccount/styles.module.css | 21 ++++ 6 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 src/views/RecoverAccount/i18n.json create mode 100644 src/views/RecoverAccount/index.tsx create mode 100644 src/views/RecoverAccount/styles.module.css diff --git a/src/App/routes/index.tsx b/src/App/routes/index.tsx index de89600d..61d690ba 100644 --- a/src/App/routes/index.tsx +++ b/src/App/routes/index.tsx @@ -191,6 +191,19 @@ const login = customWrapRoute({ }, }); +const recoverAccount = customWrapRoute({ + parent: rootLayout, + path: 'recover-account', + component: { + render: () => import('#views/RecoverAccount'), + props: {}, + }, + context: { + title: 'Recover Account', + visibility: 'is-not-authenticated', + }, +}); + const wrappedRoutes = { rootLayout, homeLayout, @@ -204,6 +217,7 @@ const wrappedRoutes = { about, pageNotFound, login, + recoverAccount, }; export const unwrappedRoutes = unwrapRoute(Object.values(wrappedRoutes)); diff --git a/src/views/Login/i18n.json b/src/views/Login/i18n.json index 74132ad9..0e324515 100644 --- a/src/views/Login/i18n.json +++ b/src/views/Login/i18n.json @@ -1,7 +1,7 @@ { "namespace": "login", "strings": { - "loginTitle":"IFRC GO - Login", + "loginTitle":"IFRC Alert-Hub - Login", "loginHeader":"Login", "loginSubHeader":"If you are staff, member or volunteer of the Red Cross Red Crescent Movement (National Societies, the IFRC and the ICRC) login with you email and password.", "loginEmailUsername":"Email", diff --git a/src/views/Login/index.tsx b/src/views/Login/index.tsx index fd6f6727..3ac7edac 100644 --- a/src/views/Login/index.tsx +++ b/src/views/Login/index.tsx @@ -117,7 +117,7 @@ export function Component() {
diff --git a/src/views/RecoverAccount/i18n.json b/src/views/RecoverAccount/i18n.json new file mode 100644 index 00000000..10fc1a6d --- /dev/null +++ b/src/views/RecoverAccount/i18n.json @@ -0,0 +1,10 @@ +{ + "namespace": "recoverAccount", + "strings": { + "pageTitle": "IFRC Alert-Hub - Recover Account", + "pageHeading": "Recover Account", + "pageDescription": "Enter the email/username you used during registration", + "emailInputLabel": "Email/Username", + "submitButtonLabel": "Submit recovery request" + } +} diff --git a/src/views/RecoverAccount/index.tsx b/src/views/RecoverAccount/index.tsx new file mode 100644 index 00000000..af79b982 --- /dev/null +++ b/src/views/RecoverAccount/index.tsx @@ -0,0 +1,106 @@ +import { useMemo } from 'react'; +import { + Button, + TextInput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + createSubmitHandler, + getErrorObject, + ObjectSchema, + requiredStringCondition, + useForm, +} from '@togglecorp/toggle-form'; + +import HCaptcha from '#components/Captcha'; +import Page from '#components/Page'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +interface FormFields { + email?: string; + captcha?: string; +} + +const defaultFormValue: FormFields = { +}; + +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType; + +const formSchema: FormSchema = { + fields: (): FormSchemaFields => ({ + email: { + required: true, + requiredValidation: requiredStringCondition, + }, + captcha: { + required: true, + requiredValidation: requiredStringCondition, + }, + }), +}; + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const strings = useTranslation(i18n); + + const { + value: formValue, + error: formError, + setFieldValue, + setError, + validate, + } = useForm(formSchema, { value: defaultFormValue }); + + const handleFormSubmit = useMemo( + () => createSubmitHandler( + validate, + setError, + // FIXME: Add form submission logic here + () => {}, + ), + [validate, setError], + ); + + const fieldError = getErrorObject(formError); + + return ( + +
+ +
+ + +
+ +
+ ); +} +Component.displayName = 'RecoverAccount'; diff --git a/src/views/RecoverAccount/styles.module.css b/src/views/RecoverAccount/styles.module.css new file mode 100644 index 00000000..ed47a26b --- /dev/null +++ b/src/views/RecoverAccount/styles.module.css @@ -0,0 +1,21 @@ +.recover-account { + .form { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: var(--go-ui-spacing-lg); + margin: 0 auto; + max-width: var(--go-ui-width-content-max); + + .submit-button { + align-self: center; + } + + .actions { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-lg); + align-items: center; + } + } +} From 427ee3a9394d0e039b24bdfa67c77534f9110b0e Mon Sep 17 00:00:00 2001 From: barshathakuri Date: Wed, 23 Oct 2024 16:23:25 +0545 Subject: [PATCH 03/30] Add my subscription page --- src/App/routes/index.tsx | 15 + src/components/Navbar/i18n.json | 2 +- src/components/Navbar/index.tsx | 6 + src/components/Navbar/styles.module.css | 1 - src/views/Home/AlertFilters/index.tsx | 76 ++-- .../AlertsMap/Sidebar/CountryDetail/index.tsx | 30 +- src/views/Home/AlertsMap/i18n.json | 3 +- src/views/Home/AlertsMap/index.tsx | 76 +++- src/views/Home/AlertsMap/styles.module.css | 25 +- src/views/Home/AlertsTable/i18n.json | 3 +- src/views/Home/AlertsTable/index.tsx | 75 +++- src/views/Home/AlertsTable/styles.module.css | 23 +- .../ActiveTableActions/i18n.json | 8 + .../ActiveTableActions/index.tsx | 42 +++ .../ArchiveTableActions/i18n.json | 7 + .../ArchiveTableActions/index.tsx | 35 ++ .../SubscriptionTableItem/index.tsx | 60 ++++ .../SubscriptionTableItem/styles.module.css | 4 + src/views/MySubscription/common.tsx | 24 ++ src/views/MySubscription/i18n.json | 11 + src/views/MySubscription/index.tsx | 170 +++++++++ src/views/MySubscription/styles.module.css | 15 + src/views/NewSubscriptionModal/i18n.json | 21 ++ src/views/NewSubscriptionModal/index.tsx | 340 ++++++++++++++++++ .../NewSubscriptionModal/styles.module.css | 13 + 25 files changed, 987 insertions(+), 98 deletions(-) create mode 100644 src/views/MySubscription/ActiveTableActions/i18n.json create mode 100644 src/views/MySubscription/ActiveTableActions/index.tsx create mode 100644 src/views/MySubscription/ArchiveTableActions/i18n.json create mode 100644 src/views/MySubscription/ArchiveTableActions/index.tsx create mode 100644 src/views/MySubscription/SubscriptionTableItem/index.tsx create mode 100644 src/views/MySubscription/SubscriptionTableItem/styles.module.css create mode 100644 src/views/MySubscription/common.tsx create mode 100644 src/views/MySubscription/i18n.json create mode 100644 src/views/MySubscription/index.tsx create mode 100644 src/views/MySubscription/styles.module.css create mode 100644 src/views/NewSubscriptionModal/i18n.json create mode 100644 src/views/NewSubscriptionModal/index.tsx create mode 100644 src/views/NewSubscriptionModal/styles.module.css diff --git a/src/App/routes/index.tsx b/src/App/routes/index.tsx index 61d690ba..8859e413 100644 --- a/src/App/routes/index.tsx +++ b/src/App/routes/index.tsx @@ -57,6 +57,20 @@ const homeLayout = customWrapRoute({ }, }); +const mySubscription = customWrapRoute({ + parent: rootLayout, + path: 'subscriptions', + component: { + render: () => import('#views/MySubscription'), + props: {}, + }, + context: { + title: 'My Subscriptions', + // TODO: Change visibility after login feature + visibility: 'anything', + }, +}); + const homeIndex = customWrapRoute({ parent: homeLayout, index: true, @@ -218,6 +232,7 @@ const wrappedRoutes = { pageNotFound, login, recoverAccount, + mySubscription, }; export const unwrappedRoutes = unwrapRoute(Object.values(wrappedRoutes)); diff --git a/src/components/Navbar/i18n.json b/src/components/Navbar/i18n.json index 78595b24..9d2e6223 100644 --- a/src/components/Navbar/i18n.json +++ b/src/components/Navbar/i18n.json @@ -6,6 +6,6 @@ "appAbout": "About", "appResources": "Resources", "headerMenuHome": "Home", - "headerMenuMySubscription": "My Subscription" + "headerMenuMySubscription": "My Subscriptions" } } diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index f84881db..de2821f0 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -79,9 +79,15 @@ function Navbar(props: Props) { > {strings.headerMenuHome} + + {strings.headerMenuMySubscription} + ); } + export default Navbar; diff --git a/src/components/Navbar/styles.module.css b/src/components/Navbar/styles.module.css index 7785e42f..f976e946 100644 --- a/src/components/Navbar/styles.module.css +++ b/src/components/Navbar/styles.module.css @@ -50,7 +50,6 @@ } .menu-item:hover { - text-decoration: underline; color: var(--go-ui-color-primary-red); } diff --git a/src/views/Home/AlertFilters/index.tsx b/src/views/Home/AlertFilters/index.tsx index c4dc0b02..ac32b497 100644 --- a/src/views/Home/AlertFilters/index.tsx +++ b/src/views/Home/AlertFilters/index.tsx @@ -55,65 +55,65 @@ const categoryLabelSelector = (category: Category) => category.label; const ALERT_ENUMS = gql` query AlertEnums { enums { - AlertInfoCertainty { - key - label - } - AlertInfoUrgency { - label - key - } - AlertInfoSeverity { - key - label - } - AlertInfoCategory { - key - label - } + AlertInfoCertainty { + key + label + } + AlertInfoUrgency { + label + key + } + AlertInfoSeverity { + key + label + } + AlertInfoCategory { + key + label + } } }`; const ADMIN_LIST = gql` query FilteredAdminList($filters:Admin1Filter, $pagination: OffsetPaginationInput) { public { - id - admin1s(filters: $filters, pagination: $pagination) { - items { - id - name - countryId - alertCount + id + admin1s(filters: $filters, pagination: $pagination) { + items { + id + name + countryId + alertCount + } } - } } - } +} `; const REGION_LIST = gql` query RegionList { public { id - regions { - items { - id - name - ifrcGoId + regions { + items { + id + name + ifrcGoId + } } - } } - } +} `; const ALL_COUNTRY_LIST = gql` query AllCountryList { - public { - id - allCountries { - name - id + public { + id + allCountries { + name + id + } } - } } `; diff --git a/src/views/Home/AlertsMap/Sidebar/CountryDetail/index.tsx b/src/views/Home/AlertsMap/Sidebar/CountryDetail/index.tsx index aac0fd6f..11b6e1f4 100644 --- a/src/views/Home/AlertsMap/Sidebar/CountryDetail/index.tsx +++ b/src/views/Home/AlertsMap/Sidebar/CountryDetail/index.tsx @@ -36,23 +36,23 @@ import styles from './styles.module.css'; const COUNTRY_DETAIL = gql` query CountryDetail($countryId: ID!) { - public { - id - country(pk: $countryId) { - id - bbox - name - iso3 - ifrcGoId - alertCount - admin1s { + public { id - countryId - filteredAlertCount - name - } + country(pk: $countryId) { + id + bbox + name + iso3 + ifrcGoId + alertCount + admin1s { + id + countryId + filteredAlertCount + name + } + } } - } } `; diff --git a/src/views/Home/AlertsMap/i18n.json b/src/views/Home/AlertsMap/i18n.json index 64c9deea..dcca6882 100644 --- a/src/views/Home/AlertsMap/i18n.json +++ b/src/views/Home/AlertsMap/i18n.json @@ -6,6 +6,7 @@ "ongoingAlertCountries": "Ongoing Alert Countries", "backToAlertsLabel": "Back to Alerts", "alertViewDetails": "View Details", - "alertInfo": "The IFRC AlertHub shows current warnings from official alerting agencies. These warnings have a start time (when the event might happen) and an end time (when it's expected to be over). The IFRC Alert Hub shows warnings that are happening right now (their start time has already passed) but aren't finished yet (their end time hasn't come yet)." + "alertInfo": "The IFRC AlertHub shows current warnings from official alerting agencies. These warnings have a start time (when the event might happen) and an end time (when it's expected to be over). The IFRC Alert Hub shows warnings that are happening right now (their start time has already passed) but aren't finished yet (their end time hasn't come yet).", + "alertNewSubscription": "New Subscription" } } diff --git a/src/views/Home/AlertsMap/index.tsx b/src/views/Home/AlertsMap/index.tsx index 0154322c..bab79f74 100644 --- a/src/views/Home/AlertsMap/index.tsx +++ b/src/views/Home/AlertsMap/index.tsx @@ -7,12 +7,19 @@ import { gql, useQuery, } from '@apollo/client'; -import { ChevronRightLineIcon } from '@ifrc-go/icons'; import { + AddLineIcon, + ChevronRightLineIcon, +} from '@ifrc-go/icons'; +import { + Button, Container, InfoPopup, } from '@ifrc-go/ui'; -import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + useBooleanState, + useTranslation, +} from '@ifrc-go/ui/hooks'; import { resolveToString } from '@ifrc-go/ui/utils'; import { isDefined, @@ -28,6 +35,7 @@ import { FilteredCountryListQueryVariables, } from '#generated/types/graphql'; import useFilterState from '#hooks/useFilterState'; +import NewSubscriptionModal from '#views/NewSubscriptionModal'; import AlertDataContext from '../AlertDataContext'; import AlertFilters from '../AlertFilters'; @@ -77,12 +85,21 @@ type AlertPointProperties = { export function Component() { const strings = useTranslation(i18n); const alertFilters = useAlertFilters(); + + const [showSubscriptionModal, { + setTrue: setShowSubscriptionModalTrue, + setFalse: setShowSubscriptionModalFalse, + }] = useBooleanState(false); + const { activeAdmin1Id, activeCountryId, activeAlertId, activeCountryDetails, activeAdmin1Details, + selectedUrgencyTypes, + selectedCertaintyTypes, + selectedSeverityTypes, } = useContext(AlertDataContext); // FIXME: We should remove useFilterState as we are not using any feature @@ -170,6 +187,22 @@ export function Component() { [totalAlertCount, activeCountryDetails, activeAdmin1Details, strings], ); + const defaultSubscription = useMemo(() => ({ + id: '', + title: '', + urgency: selectedUrgencyTypes, + severity: selectedSeverityTypes, + certainty: selectedCertaintyTypes, + country: activeCountryId, + admin1: activeAdmin1Id, + }), [ + selectedUrgencyTypes, + selectedSeverityTypes, + selectedCertaintyTypes, + activeCountryId, + activeAdmin1Id, + ]); + return ( - )} - > - {strings.mapViewAllSources} - +
+ + + )} + > + {strings.mapViewAllSources} + +
)} overlayPending pending={countryListLoading} @@ -204,6 +252,12 @@ export function Component() { filters={} withGridViewInFilter > + {showSubscriptionModal && ( + + )} ({ + id: '', + title: '', + urgency: selectedUrgencyTypes, + severity: selectedSeverityTypes, + certainty: selectedCertaintyTypes, + country: activeCountryId, + admin1: activeAdmin1Id, + }), [ + selectedUrgencyTypes, + selectedSeverityTypes, + selectedCertaintyTypes, + activeCountryId, + activeAdmin1Id, + ]); + return ( - )} - > - {strings.tableViewAllSources} - +
+ + + )} + > + {strings.tableViewAllSources} + +
)} overlayPending pending={alertInfoLoading} @@ -310,6 +357,12 @@ export function Component() { )} filters={} > + {showSubscriptionModal && ( + + )} } + variant="tertiary" + withoutDropdownIcon + > + + {strings.archiveSubscriptionActions} + + + {strings.editSubscriptionActions} + + + {strings.deleteSubscriptionActions} + + + ); +} + +export default ActiveTableActions; diff --git a/src/views/MySubscription/ArchiveTableActions/i18n.json b/src/views/MySubscription/ArchiveTableActions/i18n.json new file mode 100644 index 00000000..cd2fa016 --- /dev/null +++ b/src/views/MySubscription/ArchiveTableActions/i18n.json @@ -0,0 +1,7 @@ +{ + "namespace": "SubscriptionActions", + "strings": { + "unarchiveSubscriptionActions": "Unarchive", + "deleteSubscriptionActions": "Delete" + } +} diff --git a/src/views/MySubscription/ArchiveTableActions/index.tsx b/src/views/MySubscription/ArchiveTableActions/index.tsx new file mode 100644 index 00000000..aec03ac0 --- /dev/null +++ b/src/views/MySubscription/ArchiveTableActions/index.tsx @@ -0,0 +1,35 @@ +import { MoreOptionsIcon } from '@ifrc-go/icons'; +import { DropdownMenu } from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; + +import DropdownMenuItem from '#components/DropdownMenuItem'; + +import i18n from './i18n.json'; + +function ArchiveTableActions() { + const strings = useTranslation(i18n); + + return ( + } + variant="tertiary" + withoutDropdownIcon + > + + {strings.unarchiveSubscriptionActions} + + + {strings.deleteSubscriptionActions} + + + ); +} + +export default ArchiveTableActions; diff --git a/src/views/MySubscription/SubscriptionTableItem/index.tsx b/src/views/MySubscription/SubscriptionTableItem/index.tsx new file mode 100644 index 00000000..ee7bf395 --- /dev/null +++ b/src/views/MySubscription/SubscriptionTableItem/index.tsx @@ -0,0 +1,60 @@ +import { Container } from '@ifrc-go/ui'; + +import { + AlertInfoCertaintyEnum, + AlertInfoSeverityEnum, + AlertInfoUrgencyEnum, +} from '#generated/types/graphql'; + +import styles from './styles.module.css'; + +interface Props { + country: string | undefined; + admin1: string | undefined; + urgency?: AlertInfoUrgencyEnum[] | undefined; + certainty?: AlertInfoCertaintyEnum[] | undefined; + severity?: AlertInfoSeverityEnum[] | undefined; + title: string; + totalCount: number; + actions: React.ReactNode; +} + +function SubscriptionTableItem(props: Props) { + const { + country, + admin1, + urgency, + certainty, + severity, + title, + totalCount, + actions, + } = props; + + return ( + + ( + {totalCount} + ) + {actions} + + )} + footerContent={( + <> + {country} + {admin1} + {urgency} + {certainty} + {severity} + + )} + /> + ); +} + +export default SubscriptionTableItem; diff --git a/src/views/MySubscription/SubscriptionTableItem/styles.module.css b/src/views/MySubscription/SubscriptionTableItem/styles.module.css new file mode 100644 index 00000000..a50abf27 --- /dev/null +++ b/src/views/MySubscription/SubscriptionTableItem/styles.module.css @@ -0,0 +1,4 @@ +.subscription-detail { + background-color: var(--go-ui-color-gray-20); + padding: var(--go-ui-spacing-md); +} diff --git a/src/views/MySubscription/common.tsx b/src/views/MySubscription/common.tsx new file mode 100644 index 00000000..41a6cd25 --- /dev/null +++ b/src/views/MySubscription/common.tsx @@ -0,0 +1,24 @@ +import { + AlertInfoCertaintyEnum, + AlertInfoSeverityEnum, + AlertInfoUrgencyEnum, +} from '#generated/types/graphql'; + +export interface FrequencyOption { + label: string; + key: 'daily' | 'weekly'; +} + +// TODO: Add subscription interface from generated +export interface SubscriptionDetail { + id: string; + title: string; + country: string | undefined; + admin1: string | undefined; + urgency?: AlertInfoUrgencyEnum[] | undefined; + certainty?: AlertInfoCertaintyEnum[] | undefined; + severity?: AlertInfoSeverityEnum[] | undefined; + totalCount?: number; + sendEmail?: boolean; + frequency?: 'daily' | 'weekly' | undefined; +} diff --git a/src/views/MySubscription/i18n.json b/src/views/MySubscription/i18n.json new file mode 100644 index 00000000..dd163098 --- /dev/null +++ b/src/views/MySubscription/i18n.json @@ -0,0 +1,11 @@ +{ + "namespace": "mySubscription", + "strings": { + "mySubscription": "My Subscription", + "myNewSubscription": "New Subscription", + "createNewSubscription": "Create", + "sendViaEmailLabel": "Send via email", + "activeSubscriptionsTab": "Active Subscriptions", + "archivedSubscriptionTab": "Archive Subscriptions" + } +} \ No newline at end of file diff --git a/src/views/MySubscription/index.tsx b/src/views/MySubscription/index.tsx new file mode 100644 index 00000000..96131d9a --- /dev/null +++ b/src/views/MySubscription/index.tsx @@ -0,0 +1,170 @@ +import { + useCallback, + useState, +} from 'react'; +import { AddLineIcon } from '@ifrc-go/icons'; +import { + Button, + Container, + List, + Tab, + TabList, + TabPanel, + Tabs, +} from '@ifrc-go/ui'; +import { + useBooleanState, + useTranslation, +} from '@ifrc-go/ui/hooks'; + +import Page from '#components/Page'; + +import NewSubscriptionModal from '../NewSubscriptionModal'; +import ActiveTableActions from './ActiveTableActions'; +import ArchiveTableActions from './ArchiveTableActions'; +import { SubscriptionDetail } from './common'; +import SubscriptionTableItem from './SubscriptionTableItem'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +const subscriptionKeySelector = (subscription: SubscriptionDetail) => subscription.id; + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const strings = useTranslation(i18n); + + const data: SubscriptionDetail[] = [ + { + id: '1', + country: 'USA', + admin1: 'LA', + title: 'Earthquake Alert', + totalCount: 20, + urgency: [], + certainty: [], + severity: [], + }, + { + id: '2', + country: 'Canada', + admin1: 'Toronto', + title: 'Flood Alert', + totalCount: 30, + urgency: [], + certainty: [], + severity: [], + }, + ]; + + type TabKey = 'active' | 'archive'; + const [activeTab, setActiveTab] = useState('active'); + + const [showSubscriptionModal, { + setTrue: setShowSubscriptionModalTrue, + setFalse: setShowSubscriptionModalFalse, + }] = useBooleanState(false); + + const activeRendererParams = useCallback((_: string, value: SubscriptionDetail) => ({ + title: value.title, + totalCount: value.totalCount ?? 0, + country: value?.country, + admin1: value?.admin1, + urgency: value?.urgency, + certainty: value?.certainty, + severity: value?.severity, + actions: , + }), []); + + const archiveRendererParams = useCallback((_: string, value: SubscriptionDetail) => ({ + title: value.title, + totalCount: value.totalCount ?? 0, + country: value?.country, + admin1: value?.admin1, + urgency: value?.urgency, + certainty: value?.certainty, + severity: value?.severity, + actions: , + }), []); + + return ( + + + )} + > + {strings.myNewSubscription} + + )} + > + {showSubscriptionModal && data?.map((subscription) => ( + + ))} + + + + {strings.activeSubscriptionsTab} + + + {strings.archivedSubscriptionTab} + + + + + + + + + + + + ); +} +Component.displayName = 'MySubscription'; diff --git a/src/views/MySubscription/styles.module.css b/src/views/MySubscription/styles.module.css new file mode 100644 index 00000000..663c717b --- /dev/null +++ b/src/views/MySubscription/styles.module.css @@ -0,0 +1,15 @@ +.mySubscription { + .content { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-2xl); + + .subscriptions { + .subscription { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-lg); + } + } + } +} \ No newline at end of file diff --git a/src/views/NewSubscriptionModal/i18n.json b/src/views/NewSubscriptionModal/i18n.json new file mode 100644 index 00000000..b687099f --- /dev/null +++ b/src/views/NewSubscriptionModal/i18n.json @@ -0,0 +1,21 @@ +{ + "namespace": "mySubscriptionModal", + "strings": { + "createNewSubscription": "Create", + "sendViaEmailLabel": "Send via email", + "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", + "newSubscriptionHeading": "New Subscription", + "newSubscriptionTitle": "Title" + } +} \ No newline at end of file diff --git a/src/views/NewSubscriptionModal/index.tsx b/src/views/NewSubscriptionModal/index.tsx new file mode 100644 index 00000000..706dcd7a --- /dev/null +++ b/src/views/NewSubscriptionModal/index.tsx @@ -0,0 +1,340 @@ +import { + useCallback, + useMemo, +} from 'react'; +import { + gql, + useQuery, +} from '@apollo/client'; +import { + Button, + Checkbox, + Modal, + MultiSelectInput, + RadioInput, + SelectInput, + TextInput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { isNotDefined } from '@togglecorp/fujs'; +import { + getErrorObject, + type ObjectSchema, + type PartialForm, + requiredCondition, + requiredStringCondition, + useForm, +} from '@togglecorp/toggle-form'; + +import { + AlertEnumsAndAllCountryListQuery, + AlertEnumsAndAllCountryListQueryVariables, + AlertEnumsQuery, + FilteredAdminListQuery, + FilteredAdminListQueryVariables, +} from '#generated/types/graphql'; +import { + stringIdSelector, + stringNameSelector, +} from '#utils/selectors'; +import { + FrequencyOption, + SubscriptionDetail, +} from '#views/MySubscription/common'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +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]; + +interface AlertFilters { + key: string; + label: string; +} + +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 frequencyKeySelector = (frequency: FrequencyOption) => frequency.key; +const frequencyLabelSelector = (frequency: FrequencyOption) => frequency.label; + +const frequencyOption: FrequencyOption[] = [ + { label: 'Daily', key: 'daily' }, + { label: 'Weekly', key: 'weekly' }, +]; + +type PartialFormFields = PartialForm; + +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType + +const formSchema: FormSchema = { + fields: (): FormSchemaFields => ({ + title: { + required: true, + requiredValidation: requiredStringCondition, + }, + urgency: { + required: true, + requiredValidation: requiredCondition, + }, + severity: { + required: true, + requiredValidation: requiredCondition, + }, + certainty: { + required: true, + requiredValidation: requiredCondition, + }, + country: { + required: true, + requiredValidation: requiredCondition, + }, + admin1: { + required: true, + requiredValidation: requiredCondition, + }, + sendEmail: { + required: true, + requiredValidation: requiredCondition, + }, + frequency: { + required: true, + requiredValidation: requiredCondition, + }, + }), +}; + +interface Props { + subscription: SubscriptionDetail; + onCloseModal?: () => void; +} + +function NewSubscriptionModal(props: Props) { + const { + subscription, + onCloseModal, + } = props; + + const defaultFormValue = useMemo(() => ({ + title: subscription?.title, + urgency: subscription?.urgency, + severity: subscription?.severity, + certainty: subscription?.certainty, + sendEmail: subscription?.sendEmail, + frequency: subscription?.frequency, + country: subscription?.country, + admin1: subscription?.admin1, + }), [ + subscription, + ]); + + const { + value, + setFieldValue, + error: formError, + // setError, + // validate, + } = useForm(formSchema, { value: defaultFormValue }); + + const fieldError = getErrorObject(formError); + + const strings = useTranslation(i18n); + const { + data: alertEnumsResponse, + } = useQuery( + ALERT_ENUMS_AND_ALL_COUNTRY, + ); + + const adminQueryVariables = useMemo( + () => { + if (isNotDefined(value.country)) { + return { + filters: undefined, + // FIXME: Implement search select input + pagination: { + offset: 0, + limit: 500, + }, + }; + } + + return { + filters: { + country: { pk: value.country }, + }, + // FIXME: Implement search select input + pagination: { + offset: 0, + limit: 500, + }, + }; + }, + [value.country], + ); + + const { + data: adminResponse, + } = useQuery( + ADMIN_LIST, + { variables: adminQueryVariables, skip: isNotDefined(value.country) }, + ); + + const subscriptionCreate = useCallback(() => { + // eslint-disable-next-line no-console + console.info('create'); + }, []); + + return ( + + {strings.createNewSubscription} + + )} + footerContentClassName={styles.createButton} + contentViewType="vertical" + spacing="comfortable" + onClose={onCloseModal} + > + +
+ + + + + +
+ + +
+ ); +} + +export default NewSubscriptionModal; diff --git a/src/views/NewSubscriptionModal/styles.module.css b/src/views/NewSubscriptionModal/styles.module.css new file mode 100644 index 00000000..7c566bd5 --- /dev/null +++ b/src/views/NewSubscriptionModal/styles.module.css @@ -0,0 +1,13 @@ +.subscription-modal { + .create-button { + display: flex; + flex-direction: column; + align-items: flex-start; + } + + .filters { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); + gap: var(--go-ui-spacing-md); + } +} \ No newline at end of file From a404b503c276a83b9dbc60e798f705312c199e59 Mon Sep 17 00:00:00 2001 From: roshni73 Date: Tue, 29 Oct 2024 17:15:27 +0545 Subject: [PATCH 04/30] Add resource page --- src/views/Resources/i18n.json | 37 +++--- src/views/Resources/index.tsx | 172 ++++++++++++++++++++------ src/views/Resources/styles.module.css | 29 ++--- 3 files changed, 166 insertions(+), 72 deletions(-) diff --git a/src/views/Resources/i18n.json b/src/views/Resources/i18n.json index 7f6890f5..ca581735 100644 --- a/src/views/Resources/i18n.json +++ b/src/views/Resources/i18n.json @@ -2,18 +2,27 @@ "namespace": "resources", "strings": { "resourceHeadingTitle": "Resources", - "resourceAlerthubTitle":"IFRC Alert Hub - Resources", - "resourceHeadingDescription":"Alert Hub is an open source web project developed in collaboration with the International Federation of Red Cross and Red Crescent Societies (IFRC) as a part of the University College London (UCL) Industry Exchange Network (IXN). You can find all the source code and instructions under the following repositories.", - "resourceAlertHubAPIs":"Alert Hub APIs", - "resourceAlertHubAPIsDescription":"Alert Hub APIs enable third-party developers and rebroadcasters to access alerts and alert feed.", - "resourceLearMore":"Learn More", - "resourceAlertHubFrontendTitle":"Alert Hub Frontend", - "resourceAlertHubFrontendDescription":"This repository contains the frontend code for the IFRC Alert Hub. The goal of the IFRC Alert Hub is to ensure that communities everywhere receive the most timely and effective emergency alerting possible, and can thereby take action to safeguard their lives and livelihoods.", - "resourceAlertHubCapAggregatorTitle":"Alert Hub CAP Aggregator", - "resourceAlertHubCapAggregatorDescription":"The CAP Aggregator is an alert aggregation service built for IFRC's Alert Hub. Public alerts use the Common Alerting Protocol (CAP) Version 1.2 standard.", - "resourceAlertHubAlertManagerTitle":"Alert Hub Alert Manager", - "resourceAlertHubAlertManagerDescription":"The Alert Manager is an alert distribution service built for IFRC's Alert Hub. Public alerts use the Common Alerting Protocol (CAP) Version 1.2 standard.", - "resourceAlertHubSubscriptionSystemTitle":"Alert Hub Subscription System", - "resourceAlertHubSubscriptionSystemDescription":"This project serves as a back-end application of IFRC Alert Hub designed to work with IFRC/UCL Alert Hub CAP Aggregator. This application relies on it to get real-time updates about alerts." - } + "resourceAlerthubTitle": "IFRC Alert Hub - Resources", + "resourceHeadingDescription": "Alert Hub is an open source web project developed in collaboration with the International Federation of Red Cross and Red Crescent Societies (IFRC) as a part of the University College London (UCL) Industry Exchange Network (IXN). You can find all the source code and instructions under the following repositories.", + "earlyWarningResourcesTitle": "Early Warning Resources", + "earlyWarningResourceIfrcewea":"IFRC Early Warning, Early Action", + "earlyWarningResourceGdpcEws": "GDPC Early Warning Systems", + "earlyWarningResourceWmoCapCourse": "WMO Common Alerting Protocol Course", + "earlyWarningResourceIfrcPape": "IFRC PAPE Messages", + "faqSectionTitle": "FAQ", + "faqWhoIsTheAudience": "Who is the audience of the IFRC AlertHub?", + "faqWhoIsBehind": "Who is behind it?", + "faqHowToAccess": "How to access the API?", + "howToSubscribeTitle": "How to Subscribe?", + "ifrcRelatedLinksTitle": "IFRC related links:", + "ifrcRelatedLinksGo": "GO", + "ifrcRelatedLinksGdpc": "GDPC", + "ifrcRelatedLinksClimateCenter": "Climate Centre", + "ifrcRelatedLinksAnticipationHub": "Anticipation Hub", + "ifrcRelatedExternalLinksTitle": "External links:", + "ifrcRelatedExternalLinksGo": "WMO register of alerting authorities", + "ifrcRelatedExternalLinksCapImplementation": "CAP implementation workshop", + "ifrcRelatedExternalLinksGooglePublicAlerts": "Google Public Alerts", + "ifrcRelatedExternalLinksEarlyWarningAllInitiative":"Early Warning For All Initiative" + } } \ No newline at end of file diff --git a/src/views/Resources/index.tsx b/src/views/Resources/index.tsx index c646e482..84c44b84 100644 --- a/src/views/Resources/index.tsx +++ b/src/views/Resources/index.tsx @@ -1,3 +1,4 @@ +import { ExternalLinkLineIcon } from '@ifrc-go/icons'; import { Container } from '@ifrc-go/ui'; import { useTranslation } from '@ifrc-go/ui/hooks'; @@ -11,72 +12,161 @@ import styles from './styles.module.css'; export function Component() { const strings = useTranslation(i18n); - const resourceData = [ + const earlyWarningResources = [ { id: 1, - heading: strings.resourceAlertHubAPIs, - description: strings.resourceAlertHubAPIsDescription, + title: strings.earlyWarningResourceIfrcewea, + url: 'https://www.ifrc.org/our-work/disasters-climate-and-crises/climate-smart-disaster-risk-reduction/early-warning-early', + }, + { + id: 2, + title: strings.earlyWarningResourceGdpcEws, + url: 'https://preparecenter.org/topic/early-warning-systems/', + }, + { + id: 3, + title: strings.earlyWarningResourceWmoCapCourse, + url: 'https://etrp.wmo.int/course/view.php?id=157', + }, + { + id: 4, + title: strings.earlyWarningResourceIfrcPape, + url: 'https://www.ifrc.org/our-work/disasters-climate-and-crises/climate-smart-disaster-risk-reduction/PAPE', + }, + ]; + const frequentlyAskedQuestion = [ + { + id: 1, + title: strings.faqWhoIsTheAudience, + url: 'https://alerthub.ifrc.org/about', + }, + { + id: 2, + title: strings.faqWhoIsBehind, + url: 'https://alerthub.ifrc.org/feeds', + }, + { + id: 3, + title: strings.faqHowToAccess, url: 'https://github.com/IFRCGo/alert-hub-web-app/blob/develop/APIDOCS.md', }, + + ]; + const ifrcResources = [ + { + id: 1, + title: strings.ifrcRelatedLinksGo, + url: 'https://go.ifrc.org/', + }, { id: 2, - heading: strings.resourceAlertHubFrontendTitle, - description: strings.resourceAlertHubFrontendDescription, - url: 'https://github.com/IFRCGo/alert-hub-web-app#readme', + title: strings.ifrcRelatedLinksGdpc, + url: 'https://preparecenter.org/', }, { id: 3, - heading: strings.resourceAlertHubCapAggregatorTitle, - description: strings.resourceAlertHubCapAggregatorDescription, - url: 'https://github.com/IFRC-Alert-Hub/Alert-Hub-CAP-Aggregator#readme', + title: strings.ifrcRelatedLinksClimateCenter, + url: 'https://www.climatecentre.org/', }, { id: 4, - heading: strings.resourceAlertHubAlertManagerTitle, - description: strings.resourceAlertHubAlertManagerDescription, - url: 'https://github.com/IFRC-Alert-Hub/Alert-Hub-Alert-Manager#readme', + title: strings.ifrcRelatedLinksAnticipationHub, + url: 'https://www.anticipation-hub.org/', + }, + ]; + const externalResources = [ + { + id: 1, + title: strings.ifrcRelatedExternalLinksGo, + url: 'https://alertingauthority.wmo.int', + }, + { + id: 2, + title: strings.ifrcRelatedExternalLinksCapImplementation, + url: 'https://cap-workshop.alert-hub.org/2023/index.html', + }, + { + id: 3, + title: strings.ifrcRelatedExternalLinksGooglePublicAlerts, + url: 'https://support.google.com/publicalerts/?hl=en', }, { - id: 5, - heading: strings.resourceAlertHubSubscriptionSystemTitle, - description: strings.resourceAlertHubSubscriptionSystemDescription, - url: 'https://github.com/IFRC-Alert-Hub/Alert-Hub-Subscription-System#readme', + id: 4, + title: strings.ifrcRelatedExternalLinksEarlyWarningAllInitiative, + url: 'https://www.un.org/en/climatechange/early-warnings-for-all', }, ]; return ( - {resourceData.map( - (resource) => ( - - {strings.resourceLearMore} - - )} - > - {resource.description} - - ), - )} + {earlyWarningResources.map((resource) => ( + } + external + > + {resource.title} + + ))} + + + {frequentlyAskedQuestion.map((faq) => ( + } + external + > + {faq.title} + + ))} + + + + {ifrcResources.map((externalLink) => ( + } + external + > + {externalLink.title} + + ))} + + + + {externalResources.map((externalLink) => ( + } + external + > + {externalLink.title} + + ))} ); diff --git a/src/views/Resources/styles.module.css b/src/views/Resources/styles.module.css index 001a5256..21876c86 100644 --- a/src/views/Resources/styles.module.css +++ b/src/views/Resources/styles.module.css @@ -1,21 +1,16 @@ .resources { - .resources-card { - display: flex; - flex-direction: column; - gap: var(--go-ui-spacing-sm); - border-radius: var(--go-ui-border-radius-lg); - box-shadow: var(--go-ui-box-shadow-md); - padding: var(--go-ui-spacing-md); - - .resources-item { - text-decoration: none; - color: var(--go-ui-color-primary-red); - font-weight: var(--go-ui-font-weight-medium); - } + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-lg); - .resources-item:hover { - text-decoration: underline; - color: var(--go-ui-color-primary-red); - } + .resource-link{ + display: flex; + padding-bottom: var(--go-ui-spacing-md); + text-decoration: none; + font-weight: var(--go-ui-font-weight-normal); + } + .resource-link:hover { + text-decoration: underline; + color: var(--go-ui-color-red-hover); } } From 7306393020a55f0168918fdbbad127c068e317e5 Mon Sep 17 00:00:00 2001 From: roshni73 Date: Tue, 12 Nov 2024 12:18:14 +0545 Subject: [PATCH 05/30] Add Validation Email page --- src/App/routes/index.tsx | 13 ++ src/views/Login/index.tsx | 2 +- src/views/ResendValidationEmail/i18n.json | 10 ++ src/views/ResendValidationEmail/index.tsx | 111 ++++++++++++++++++ .../ResendValidationEmail/styles.module.css | 21 ++++ 5 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 src/views/ResendValidationEmail/i18n.json create mode 100644 src/views/ResendValidationEmail/index.tsx create mode 100644 src/views/ResendValidationEmail/styles.module.css diff --git a/src/App/routes/index.tsx b/src/App/routes/index.tsx index 8859e413..adfd2fe4 100644 --- a/src/App/routes/index.tsx +++ b/src/App/routes/index.tsx @@ -217,6 +217,18 @@ const recoverAccount = customWrapRoute({ visibility: 'is-not-authenticated', }, }); +const resendValidationEmail = customWrapRoute({ + parent: rootLayout, + path: 'resend-validation-email', + component: { + render: () => import('#views/ResendValidationEmail'), + props: {}, + }, + context: { + title: 'Resend Validation Email', + visibility: 'is-not-authenticated', + }, +}); const wrappedRoutes = { rootLayout, @@ -232,6 +244,7 @@ const wrappedRoutes = { pageNotFound, login, recoverAccount, + resendValidationEmail, mySubscription, }; diff --git a/src/views/Login/index.tsx b/src/views/Login/index.tsx index 3ac7edac..6ae0dd87 100644 --- a/src/views/Login/index.tsx +++ b/src/views/Login/index.tsx @@ -124,7 +124,7 @@ export function Component() { {strings.loginForgotUserPass} diff --git a/src/views/ResendValidationEmail/i18n.json b/src/views/ResendValidationEmail/i18n.json new file mode 100644 index 00000000..df3c4948 --- /dev/null +++ b/src/views/ResendValidationEmail/i18n.json @@ -0,0 +1,10 @@ +{ + "namespace": "resendValidationEmail", + "strings": { + "pageTitle": "IFRC Alert-Hub - Resend Validation Email", + "pageHeading": "Resend Validation Email", + "pageDescription": "Enter the email you used during registration", + "emailInputLabel": "Email", + "submitButtonLabel": "Resend" + } +} diff --git a/src/views/ResendValidationEmail/index.tsx b/src/views/ResendValidationEmail/index.tsx new file mode 100644 index 00000000..ff6308f7 --- /dev/null +++ b/src/views/ResendValidationEmail/index.tsx @@ -0,0 +1,111 @@ +import { useMemo } from 'react'; +import { + Button, + TextInput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + createSubmitHandler, + getErrorObject, + type ObjectSchema, + requiredStringCondition, + useForm, +} from '@togglecorp/toggle-form'; + +import HCaptcha from '#components/Captcha'; +import Page from '#components/Page'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +interface FormFields { + email?: string; + captcha?:string; +} + +const defaultFormValue: FormFields = { +}; + +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType + +const formSchema: FormSchema = { + fields: (): FormSchemaFields => ({ + email: { + required: true, + requiredValidation: requiredStringCondition, + }, + captcha: { + required: true, + requiredValidation: requiredStringCondition, + }, + }), +}; + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const strings = useTranslation(i18n); + + const { + value: formValue, + error: formError, + setFieldValue, + setError, + validate, + } = useForm(formSchema, { value: defaultFormValue }); + + const handleFormSubmit = useMemo( + () => createSubmitHandler( + validate, + setError, + // FIXME: Add Submit logic here + () => {}, + ), + [validate, setError], + ); + + const fieldError = getErrorObject(formError); + + return ( + +
+ + +
+ + +
+ + +
+ ); +} + +Component.displayName = 'ResendValidationEmail'; diff --git a/src/views/ResendValidationEmail/styles.module.css b/src/views/ResendValidationEmail/styles.module.css new file mode 100644 index 00000000..813c615b --- /dev/null +++ b/src/views/ResendValidationEmail/styles.module.css @@ -0,0 +1,21 @@ +.resend-validation-email { + .form { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: var(--go-ui-spacing-lg); + margin: 0 auto; + max-width: var(--go-ui-width-content-max); + + .submit-button { + align-self: center; + } + + .actions { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-lg); + align-items: center; + } + } +} From 86369d1ca857262366c3f2796c3f0e3cc0cc4351 Mon Sep 17 00:00:00 2001 From: roshni73 Date: Wed, 13 Nov 2024 11:38:57 +0545 Subject: [PATCH 06/30] Add Cookie Policy Page --- src/App/routes/index.tsx | 15 + src/components/GlobalFooter/i18n.json | 4 +- src/components/GlobalFooter/index.tsx | 12 + src/views/CookiePolicy/i18n.json | 65 ++++ src/views/CookiePolicy/index.tsx | 375 +++++++++++++++++++++++ src/views/CookiePolicy/styles.module.css | 36 +++ src/views/RootLayout/i18n.json | 8 + src/views/RootLayout/index.tsx | 61 +++- src/views/RootLayout/styles.module.css | 10 + 9 files changed, 584 insertions(+), 2 deletions(-) create mode 100644 src/views/CookiePolicy/i18n.json create mode 100644 src/views/CookiePolicy/index.tsx create mode 100644 src/views/CookiePolicy/styles.module.css create mode 100644 src/views/RootLayout/i18n.json diff --git a/src/App/routes/index.tsx b/src/App/routes/index.tsx index adfd2fe4..f51e8eec 100644 --- a/src/App/routes/index.tsx +++ b/src/App/routes/index.tsx @@ -217,6 +217,7 @@ const recoverAccount = customWrapRoute({ visibility: 'is-not-authenticated', }, }); + const resendValidationEmail = customWrapRoute({ parent: rootLayout, path: 'resend-validation-email', @@ -230,6 +231,19 @@ const resendValidationEmail = customWrapRoute({ }, }); +const cookiePolicy = customWrapRoute({ + parent: rootLayout, + path: 'cookie-policy', + component: { + render: () => import('#views/CookiePolicy'), + props: {}, + }, + context: { + title: 'Cookie Policy', + visibility: 'anything', + }, +}); + const wrappedRoutes = { rootLayout, homeLayout, @@ -246,6 +260,7 @@ const wrappedRoutes = { recoverAccount, resendValidationEmail, mySubscription, + cookiePolicy, }; export const unwrappedRoutes = unwrapRoute(Object.values(wrappedRoutes)); diff --git a/src/components/GlobalFooter/i18n.json b/src/components/GlobalFooter/i18n.json index d9d69632..3fabdd68 100644 --- a/src/components/GlobalFooter/i18n.json +++ b/src/components/GlobalFooter/i18n.json @@ -11,6 +11,8 @@ "footerContactUs":"Contact Us", "footerIFRC":"© IFRC Alert Hub {year} v{appVersion}", "globalFindOut": "Find Out More", - "globalHelpfulLinks": "Helpful links" + "globalHelpfulLinks": "Helpful links", + "policies": "Policies", + "cookiePolicy": "Cookie Policy" } } diff --git a/src/components/GlobalFooter/index.tsx b/src/components/GlobalFooter/index.tsx index c41b278c..70d21710 100644 --- a/src/components/GlobalFooter/index.tsx +++ b/src/components/GlobalFooter/index.tsx @@ -85,6 +85,18 @@ function GlobalFooter(props: Props) { +
+ + {strings.policies} + +
+ + {strings.cookiePolicy} + +
+
{strings.globalHelpfulLinks} diff --git a/src/views/CookiePolicy/i18n.json b/src/views/CookiePolicy/i18n.json new file mode 100644 index 00000000..16576458 --- /dev/null +++ b/src/views/CookiePolicy/i18n.json @@ -0,0 +1,65 @@ +{ + "namespace": "cookiePolicy", + "strings": { + "cookiePolicyTitle": "Cookie Policy", + "disclaimerTitle": "Disclaimer", + "disclaimerDescription": "All the information contained in the website is Copyright © The International Federation of Red Cross and Red Crescent Societies. All rights reserved.", + "useOfOurInformationTitle": "Use Of Our Information", + "useOfOurInformationDescription1": "To use text information from this website, please obtain prior permission from the IFRC.", + "useOfOurInformationDescription2": "To use photographs from this website, please contact our {termsLink}.", + "useOfOurInformationAudiovisualLink": "audiovisual department", + "useOfOurInformationDescription3": "If you have any question about this website, or if you find any errors, please notify us.", + "useOfOurInformationDescriptionLink": "Click here for important information about fraudulent websites and emails.", + "ourPrivacyPolicyHeading": "Our Privacy Policy", + "ourPrivacyPolicyContent": "This website is operated by the Federation. The Federation takes your privacy and the security of our personal information-meaning and information relating to an identified or identifiable individual seriously. Please take a moment to read this privacy and learn about the Federation's general privacy and information security principles. Any changes to this Privacy Policy will be posted on this page. The version of the policy was published on {publishedDay} {publishedDate} {publishedYear}.", + "dataCollectedByAccessingHeading": "A) Data collected by accessing our sites", + "informationProvideHeading": "1. Information you provide", + "informationProvideDescription1": "On certain IFRC.org webpages, your personal data may be requested. For instance, it is necessary to obtain your email address, name and other data to sign up for a newsletter or make a donation. It is important to note that some of the features on IFRC.org sites require the use of third-party providers (please see Section F below for more details).", + "informationProvideDescription2": "You will also need to provide some personal data if you use the \"Contact us\" link at the bottom of our pages. If there are any specific changes (relative to this privacy policy) to how your data is handled on those IFRC.org, those changes will be specified in the privacy policies located on the respective site.", + "automaticallyCollectedHeading": "2. Automatically or routinely collected", + "automaticallyCollectedDescription": "Just by visiting IFRC.org, certain metadata may still be collected and processed while we have worked to minimize data collection, the following may be collected (and processed for the reasons set out in Section B of this Privacy Policy):", + "automaticallyCollectedList1": "your internet protocol(IP) address (which may reveal your approximate location and time zone)", + "automaticallyCollectedList2": "devices ID ( which indicates the type of device being used to access the platform and may also reveal the operating system used)", + "automaticallyCollectedList3": "dates and time of use", + "automaticallyCollectedList4": "requested URL", + "automaticallyCollectedList5": "Operating system you are using", + "ifrcLimitedCookiesAnalyticHeading": "3. IFRC.org also makes limited use of cookies and analytics as further described below", + "ifrcLimitedCookiesAnalyticDescription": "Cookies are text files that contain small amounts of information that we may store on your device when you use our site. Cookies enable us to monitor site traffic and estimate our audience size and usage pattern as well as allowing us to store information about your preferences to allow us to customize our site according to those preferences. They also help us to improve our site and deliver a more personalized service. We use the following types of cookies in connection with our site:", + "ifrcLimitedCookiesAnalyticList1": "Strictly Necessary: These cookies enable our site to function correctly and deliver the services and products that visitors have requested. These cookies do not gather information about the visitors that could be used for marketing or remembering sites that visitors have visited on the internet.", + "ifrcLimitedCookiesAnalyticList2": "Performance and Functionality: These cookies do things like remembering a visitor’s preferred language, understand their preferences, and associates users to any forms visitor’s submit to us to enable pre-completion of subsequent forms.", + "ifrcLimitedCookiesAnalyticList3": "Analytical: These cookies help us to analyze how our site is used, such as allowing us to compile reports on website activity and providing us other services relating to website activity and internet usage.", + "ifrcLimitedCookiesAnalyticList4": "User Authentication: These social media cookies allow visitors to login to share and promote their activities via social media.", + "ifrcLimitedCookiesAnalyticDescription2": "Our site will issue cookies to your device when you log on to the site. Cookies may either be permanent (i.e. remaining on your device until you delete them) or temporary (i.e. remaining only until you close your browser). Most browsers permit you to reject or delete cookies.", + "ifrcLimitedCookiesAnalyticDescription3": "Certain browsers and devices allow you to block cookies. If you do choose to block cookies, you may not be able to use certain features on our site and/or may be required to re-enter information more frequently to use certain services on the site.", + "howInformationUsedHeading": "B) How is this information used?", + "howInformationUsedDescription": "This information is used to improve our service, compile statistics (including on consultation) and to communicate with you. More specifically, it is used:", + "howInformationUsedDescriptionList1": "To reply to your requests for information", + "howInformationUsedDescriptionList2": "to enable you to use certain interactive functions (e.g. download data,save files)", + "howInformationUsedDescriptionList3": "To provide services or give help", + "howInformationUsedDescriptionList4": "To improve our website, communication, products and services; to compile statistics on consultation, and to understand what content matters most to the community", + "howInformationUsedDescriptionList5": "To maintain the security and integrity", + "howInformationUsedDescriptionList6": "To ensure that content is presented in the way that suits you best", + "dataAccessSharingHeading": "C) Data access and sharing", + "dataAccessSharingDescription1": "As a general rule, the Federation will not disclose any information identifying you personally, except with your permission. Information you provide to the Federation will not be sold to third parties or used in a manner inconsistent with the aims of the website.", + "dataAccessSharingDescription2": "Information may be shared with the Federation membership or other components of the International Red Cross and Red Crescent Movement, to further facilitate humanitarian assistance. Some information identifying donors to the Federation is forwarded to third parties for purely technical and administrative processing (such as for payment processing). These parties are not authorized to distribute, sell or rent user information.", + "dataAccessSharingDescription3": "Further, your data will only be accessible to those staff (and other authorized individuals) that have a legitimate need to have such access.", + "storageSecurityQuestionsAboutDataHeading": "D) Storage ,security, questions about your data", + "storageSecurityQuestionsDataDescription1": "The Federation uses secure systems and implements appropriate technical and organizational measures to safeguard your personal information and protect it against unauthorized or unlawful processing, accidental loss, destruction, or damage. While we take reasonable measures to protect your personal information, please be aware that no system can guarantee 100% security.", + "storageSecurityQuestionsDataDescription2": "Where it is necessary for your personal data to be shared with third parties, we will take reasonable measures to ensure that your personal data continues to be treated securely.", + "storageSecurityQuestionsAboutDataDescription3": "These measures may be specific contractual provisions requiring those third parties to implement no less equivalent security measures than those of the Federation. Please be aware of this when sending any communications. We cannot (and do not) guarantee absolute security. Under certain circumstances, you may request to review personal information we hold about you. You may also object to certain personal data processing and/or request the correction or deletion of such personal data according to our {termsLink}. More detail about data subject requests can be found in Section 3 of that Policy.", + "policyProtectionOfPersonalDataLink": "Policy on the Protection of Personal Data", + "storageSecurityQuestionsAboutDataDescription4": "For further information on how the IFRC processes personal data, please consult our {termsLink}. Here you may also find general information, useful tools for humanitarians, and information specific to procurements and other partners of the Federation.", + "dataProtectionPageLink": "Data Protection page", + "storageSecurityQuestionsDataDescription5": "Please contact the following department for personal data questions related to:", + "storageSecurityQuestionsDataGoEnquires": "IFRC-GO enquiries: {termsLink}", + "storageSecurityQuestionsDataDonations": "Donations: {termsLink}", + "storageSecurityQuestionsDataRecruitment": "Recruitment: {termsLink}", + "securityQuestionsWebpageCollection": "General webpage collection: {termsLink}", + "storageSecurityQuestionsDataEnquires": "Other data protection enquiries: {termsLink}", + "privilegesAndImmunitiesHeading": "E) Privileges and immunities", + "privilegesAndImmunitiesDescription": "Nothing in or relating to this Privacy Policy or this site shall be deemed a waiver of any of the privileges and immunities of the Federation.", + "noteOnLinksToExternalWebsitesHeading": "F) Notes on links to external websites", + "noteOnLinksToExternalWebsitesDescription1": "Please note that this privacy policy only applies to the IFRC.org website. It does not apply to pages hosted by other organizations, including National Red Cross and Red Crescent Societies and the International Committee of the Red Cross, which have their own privacy policies.", + "noteOnLinksToExternalWebsitesDescription2": "Additionally, some IFRC.org webpages may: 1) incorporate or redirect to third-party service providers (such as Mailchimp or Lyris for the management of mailing lists, Podio for advocacy engagement, or Cornerstone for recruitment) and 2) contain links to third-party sites, such as Facebook, YouTube, or Twitter, for example. These third-party and external sites (and any other non-IFRC.org sites) will typically have their own privacy policies and data handling practices that the IFRC does not control. You are strongly encouraged to review those relevant privacy policies." + } +} diff --git a/src/views/CookiePolicy/index.tsx b/src/views/CookiePolicy/index.tsx new file mode 100644 index 00000000..8f5ab8aa --- /dev/null +++ b/src/views/CookiePolicy/index.tsx @@ -0,0 +1,375 @@ +import { + useRef, + useState, +} from 'react'; +import { + Container, + Tab, + TabList, + Tabs, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + resolveToComponent, + resolveToString, +} from '@ifrc-go/ui/utils'; + +import Link from '#components/Link'; +import Page from '#components/Page'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +type TitlesOptionKey = 'disclaimer' | 'use-of-our-information' | 'our-privacy-policy'; + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const strings = useTranslation(i18n); + const [activeTitlesOption, setActiveTitleOption] = useState('disclaimer'); + + const disclaimerRef = useRef(null); + const useOfOurInformationRef = useRef(null); + const ourPrivacyPolicyRef = useRef(null); + + const handleTabChange = (newTab: TitlesOptionKey) => { + setActiveTitleOption(newTab); + + const tabRefs = { + disclaimer: disclaimerRef, + 'use-of-our-information': useOfOurInformationRef, + 'our-privacy-policy': ourPrivacyPolicyRef, + }; + tabRefs[newTab]?.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + return ( + +
+ + + + {strings.disclaimerTitle} + + + {strings.useOfOurInformationTitle} + + + {strings.ourPrivacyPolicyHeading} + + + +
+ + +
{strings.useOfOurInformationDescription1}
+
+ { resolveToComponent( + strings.useOfOurInformationDescription2, + { + termsLink: ( + + {strings.useOfOurInformationAudiovisualLink} + + ), + }, + )} +
+
{strings.useOfOurInformationDescription3}
+
+ + {strings.useOfOurInformationDescriptionLink} + +
+ + )} + withHeaderBorder + withInternalPadding + /> + +
+ {resolveToString(strings.ourPrivacyPolicyContent, { + publishedDay: 'November', + publishedDate: 29, + publishedYear: 2021, + })} +
+ + +
{strings.informationProvideDescription1}
+
{strings.informationProvideDescription2}
+ + )} + /> + +
{strings.automaticallyCollectedDescription}
+
    +
  • {strings.automaticallyCollectedList1}
  • +
  • {strings.automaticallyCollectedList2}
  • +
  • {strings.automaticallyCollectedList3}
  • +
  • {strings.automaticallyCollectedList4}
  • +
  • {strings.automaticallyCollectedList5}
  • +
+ + )} + /> + +
{strings.ifrcLimitedCookiesAnalyticDescription}
+
    +
  • {strings.ifrcLimitedCookiesAnalyticList1}
  • +
  • {strings.ifrcLimitedCookiesAnalyticList2}
  • +
  • {strings.ifrcLimitedCookiesAnalyticList3}
  • +
  • {strings.ifrcLimitedCookiesAnalyticList4}
  • +
+
{strings.ifrcLimitedCookiesAnalyticDescription2}
+
{strings.ifrcLimitedCookiesAnalyticDescription3}
+ + )} + /> +
+ +
{strings.howInformationUsedDescription}
+
    +
  • {strings.howInformationUsedDescriptionList1}
  • +
  • {strings.howInformationUsedDescriptionList2}
  • +
  • {strings.howInformationUsedDescriptionList3}
  • +
  • {strings.howInformationUsedDescriptionList4}
  • +
  • {strings.howInformationUsedDescriptionList5}
  • +
  • {strings.howInformationUsedDescriptionList6}
  • +
+ + )} + /> + +
{strings.dataAccessSharingDescription1}
+
{strings.dataAccessSharingDescription2}
+
{strings.dataAccessSharingDescription3}
+ + )} + /> + +
{strings.storageSecurityQuestionsDataDescription1}
+
{strings.storageSecurityQuestionsDataDescription2}
+
+ { resolveToComponent( + strings.storageSecurityQuestionsAboutDataDescription3, + { + termsLink: ( + + {strings.policyProtectionOfPersonalDataLink} + + ), + }, + )} +
+
+ {resolveToComponent( + strings.storageSecurityQuestionsAboutDataDescription4, + { + termsLink: ( + + {strings.dataProtectionPageLink} + + ), + }, + )} +
+
{strings.storageSecurityQuestionsDataDescription5}
+
+ + { resolveToComponent( + strings.storageSecurityQuestionsDataGoEnquires, + { + termsLink: ( + + im@ifrc.org + + ), + }, + )} + +
+
+ + { resolveToComponent( + strings.storageSecurityQuestionsDataDonations, + { + termsLink: ( + + prd@ifrc.org + + ), + }, + )} + +
+
+ + {resolveToComponent( + strings.storageSecurityQuestionsDataRecruitment, + { + termsLink: ( + + ask.hr@ifrc.org + + ), + }, + )} + +
+
+ + { resolveToComponent( + strings.securityQuestionsWebpageCollection, + { + termsLink: ( + + webteam@ifrc.org + + ), + }, + )} + +
+
+ + { resolveToComponent( + strings.storageSecurityQuestionsDataEnquires, + { + termsLink: ( + + dataprotection@ifrc.org + + ), + }, + )} + +
+ + )} + /> + + {strings.privilegesAndImmunitiesDescription} +
+ )} + /> + +
{strings.noteOnLinksToExternalWebsitesDescription1}
+
{strings.noteOnLinksToExternalWebsitesDescription2}
+ + )} + /> +
+
+
+ + ); +} + +Component.displayName = 'CookiePolicy'; diff --git a/src/views/CookiePolicy/styles.module.css b/src/views/CookiePolicy/styles.module.css new file mode 100644 index 00000000..ce73fa2c --- /dev/null +++ b/src/views/CookiePolicy/styles.module.css @@ -0,0 +1,36 @@ +.cookie-page { + display: flex; + gap: var(--go-ui-spacing-xl); + + .side-titles { + display: flex; + flex-direction: column; + flex-shrink: 0; + gap: var(--go-ui-spacing-md); + padding: var(--go-ui-spacing-sm); + } + + .header-description { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-md); + } + + .main-content { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-md); + + .first-level-content { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-md); + + .second-level-content { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-sm); + } + } + } +} diff --git a/src/views/RootLayout/i18n.json b/src/views/RootLayout/i18n.json new file mode 100644 index 00000000..c084d307 --- /dev/null +++ b/src/views/RootLayout/i18n.json @@ -0,0 +1,8 @@ +{ + "namespace": "root", + "strings": { + "cookiesBannerDescription": "By entering this website, you consent to the use of technologies, such as cookies and analytics. These will be used to analyse traffic to the website, allowing us to understand visitor preferences and improve our services", + "cookiesBannerLearnMore": "Learn more", + "cookiesBannerIAccept": "I Accept" + } +} diff --git a/src/views/RootLayout/index.tsx b/src/views/RootLayout/index.tsx index 004241d0..722cda08 100644 --- a/src/views/RootLayout/index.tsx +++ b/src/views/RootLayout/index.tsx @@ -8,11 +8,21 @@ import { Outlet, useNavigation, } from 'react-router-dom'; -import { AlertContainer } from '@ifrc-go/ui'; +import { AlertInformationLineIcon } from '@ifrc-go/icons'; +import { + AlertContainer, + Button, + Container, + PageContainer, +} from '@ifrc-go/ui'; import { Language, LanguageContext, } from '@ifrc-go/ui/contexts'; +import { + useBooleanState, + useTranslation, +} from '@ifrc-go/ui/hooks'; import { _cs, listToGroupList, @@ -21,13 +31,16 @@ import { } from '@togglecorp/fujs'; import GlobalFooter from '#components/GlobalFooter'; +import Link from '#components/Link'; import Navbar from '#components/Navbar'; import useDebouncedValue from '#hooks/useDebouncedValue'; +import i18n from './i18n.json'; import styles from './styles.module.css'; // eslint-disable-next-line import/prefer-default-export export function Component() { + const strings = useTranslation(i18n); const { state } = useNavigation(); const isLoading = state === 'loading'; const isLoadingDebounced = useDebouncedValue(isLoading); @@ -38,6 +51,17 @@ export function Component() { setStrings, } = useContext(LanguageContext); + // FIXME: To be made functional after the implications of cookie rejections are finalized + const [ + isCookiesBannerVisible, + { setFalse: hideCookiesBanner }, + ] = useBooleanState(false); + + const handleClick = useCallback(() => { + // FIXME: Add cookies permission to session storage + hideCookiesBanner(); + }, [hideCookiesBanner]); + const fetchLanguage = useCallback(async (lang: Language) => { setLanguagePending(true); const resource = await import(`./translations/${lang}.json`); @@ -103,6 +127,41 @@ export function Component() { className={styles.footer} /> + {isCookiesBannerVisible && ( +
+ {isCookiesBannerVisible && ( + + + )} + spacing="comfortable" + actions={( + <> + + {strings.cookiesBannerLearnMore} + + + + )} + /> + + )} +
+ )} ); } diff --git a/src/views/RootLayout/styles.module.css b/src/views/RootLayout/styles.module.css index faa02891..cbd7132c 100644 --- a/src/views/RootLayout/styles.module.css +++ b/src/views/RootLayout/styles.module.css @@ -4,6 +4,16 @@ flex-direction: column; height: 100vh; + .cookies-banner { + background-color: var(--go-ui-color-primary-blue); + width: 100%; + color: var(--go-ui-color-white); + + .alert-info-icon { + font-size: 3rem; + } + } + .navigation-loader { position: fixed; transition: var(--go-ui-duration-animation-medium) width ease-out; From a0dbb26690c389bcfb2e66bf83684c77f39d1bcd Mon Sep 17 00:00:00 2001 From: barshathakuri Date: Thu, 7 Nov 2024 11:42:07 +0545 Subject: [PATCH 07/30] Add historical alerts table --- src/App/routes/index.tsx | 14 + src/components/Navbar/i18n.json | 3 +- src/components/Navbar/index.tsx | 6 + .../HistoricalAlerts/AlertActions/i18n.json | 6 + .../HistoricalAlerts/AlertActions/index.tsx | 56 ++ .../AlertActions/styles.module.css | 6 + src/views/HistoricalAlerts/i18n.json | 33 ++ src/views/HistoricalAlerts/index.tsx | 495 ++++++++++++++++++ src/views/HistoricalAlerts/styles.module.css | 68 +++ src/views/MySubscription/index.tsx | 5 +- src/views/NewSubscriptionModal/index.tsx | 2 +- 11 files changed, 689 insertions(+), 5 deletions(-) create mode 100644 src/views/HistoricalAlerts/AlertActions/i18n.json create mode 100644 src/views/HistoricalAlerts/AlertActions/index.tsx create mode 100644 src/views/HistoricalAlerts/AlertActions/styles.module.css create mode 100644 src/views/HistoricalAlerts/i18n.json create mode 100644 src/views/HistoricalAlerts/index.tsx create mode 100644 src/views/HistoricalAlerts/styles.module.css diff --git a/src/App/routes/index.tsx b/src/App/routes/index.tsx index f51e8eec..e8d8987c 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 9d2e6223..7ffcd233 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 de2821f0..eb1ebc2b 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 00000000..28dcdf39 --- /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 00000000..aa20e83a --- /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 00000000..39705f67 --- /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 00000000..a1027bf4 --- /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 00000000..32dad1f6 --- /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 00000000..2b1da9d6 --- /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 96131d9a..1bab16b3 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; } From d251c3116ebe4695d161f6824f33a859cb5eed7e Mon Sep 17 00:00:00 2001 From: barshathakuri Date: Wed, 20 Nov 2024 16:10:30 +0545 Subject: [PATCH 08/30] Login integration --- src/App/Auth.tsx | 45 ++++++++++ src/App/index.tsx | 93 +++++++++++++++++-- src/App/routes/common.tsx | 47 ++++++++++ src/App/routes/index.tsx | 40 +++++---- src/App/styles.module.css | 7 ++ src/components/Link/index.tsx | 2 +- src/components/Navbar/i18n.json | 4 +- src/components/Navbar/index.tsx | 92 ++++++++++++++++--- src/contexts/user.tsx | 17 +--- src/hooks/{ => domain}/useAuth.ts | 2 +- src/index.tsx | 1 + src/utils/user.ts | 31 +++++++ src/views/Login/i18n.json | 3 +- src/views/Login/index.tsx | 142 ++++++++++++++++++++++++------ 14 files changed, 443 insertions(+), 83 deletions(-) create mode 100644 src/App/Auth.tsx create mode 100644 src/App/routes/common.tsx create mode 100644 src/App/styles.module.css rename src/hooks/{ => domain}/useAuth.ts (81%) create mode 100644 src/utils/user.ts diff --git a/src/App/Auth.tsx b/src/App/Auth.tsx new file mode 100644 index 00000000..0cbc1ee7 --- /dev/null +++ b/src/App/Auth.tsx @@ -0,0 +1,45 @@ +import { + Fragment, + type ReactElement, +} from 'react'; +import { Navigate } from 'react-router-dom'; + +import useAuth from '#hooks/domain/useAuth'; + +import { type ExtendedProps } from './routes/common'; + +interface Props { + children: ReactElement, + context: ExtendedProps, + absolutePath: string, +} +function Auth(props: Props) { + const { + context, + children, + absolutePath, + } = props; + + const { isAuthenticated } = useAuth(); + + if (context.visibility === 'is-authenticated' && !isAuthenticated) { + return ( + + ); + } + if (context.visibility === 'is-not-authenticated' && isAuthenticated) { + return ( + + ); + } + + return ( + + {children} + + ); +} + +export default Auth; diff --git a/src/App/index.tsx b/src/App/index.tsx index 8e16d45e..e703de75 100644 --- a/src/App/index.tsx +++ b/src/App/index.tsx @@ -10,6 +10,10 @@ import { createBrowserRouter, RouterProvider, } from 'react-router-dom'; +import { + gql, + useQuery, +} from '@apollo/client'; import { AlertContext, AlertContextProps, @@ -27,6 +31,11 @@ import mapboxgl from 'mapbox-gl'; import { mapboxToken } from '#config'; import RouteContext from '#contexts/route'; +import UserContext, { + UserAuth, + UserContextProps, +} from '#contexts/user'; +import { MeQuery } from '#generated/types/graphql'; import { KEY_LANGUAGE_STORAGE } from '#utils/constants'; import { getFromStorage, @@ -35,6 +44,25 @@ import { import wrappedRoutes, { unwrappedRoutes } from './routes'; +import styles from './styles.module.css'; + +const ME = gql` + query Me { + public { + me { + city + country + displayName + email + firstName + id + lastName + phoneNumber + } + } + } +`; + const router = createBrowserRouter(unwrappedRoutes); mapboxgl.accessToken = mapboxToken; mapboxgl.setRTLTextPlugin( @@ -66,13 +94,51 @@ function App() { }, []); const setAndStoreCurrentLanguage = useCallback( - (newLanugage: Language) => { - setCurrentLanguage(newLanugage); - setToStorage(KEY_LANGUAGE_STORAGE, newLanugage); + (newLanguage: Language) => { + setCurrentLanguage(newLanguage); + setToStorage(KEY_LANGUAGE_STORAGE, newLanguage); }, [], ); + // AUTH + + const [userAuth, setUserAuth] = useState(); + + const removeUserAuth = useCallback(() => { + setUserAuth(undefined); + }, []); + + // Hydration + useEffect(() => { + const language = getFromStorage(KEY_LANGUAGE_STORAGE); + setCurrentLanguage(language ?? 'en'); + }, []); + + const { + loading: meLoading, + } = useQuery( + ME, + { + onCompleted: (response) => { + if (response.public.me) { + setUserAuth(response.public.me); + } else { + removeUserAuth(); + } + }, + }, + ); + + const userContextValue = useMemo( + () => ({ + userAuth, + setUserAuth, + removeUserAuth, + }), + [userAuth, removeUserAuth], + ); + const registerLanguageNamespace = useCallback( (namespace: string, fallbackStrings: Record) => { setStrings( @@ -185,13 +251,24 @@ function App() { removeAlert, }), [alerts, addAlert, updateAlert, removeAlert]); + if (meLoading) { + return ( + // FIXME: Use translation +
+ Checking user session... +
+ ); + } + return ( - - - - - + + + + + + + ); } diff --git a/src/App/routes/common.tsx b/src/App/routes/common.tsx new file mode 100644 index 00000000..bfad45ea --- /dev/null +++ b/src/App/routes/common.tsx @@ -0,0 +1,47 @@ +import { + type MyInputIndexRouteObject, + type MyInputNonIndexRouteObject, + type MyOutputIndexRouteObject, + type MyOutputNonIndexRouteObject, + wrapRoute, +} from '#utils/routes'; +import { Component as RootLayout } from '#views/RootLayout'; + +import Auth from '../Auth'; +import PageError from '../PageError'; + +export type ExtendedProps = { + title: string, + visibility: 'is-authenticated' | 'is-not-authenticated' | 'anything', + permissions?: ( + params: Record | undefined | null, + ) => boolean; +}; + +interface CustomWrapRoute { + ( + myRouteOptions: MyInputIndexRouteObject + ): MyOutputIndexRouteObject + ( + myRouteOptions: MyInputNonIndexRouteObject + ): MyOutputNonIndexRouteObject +} + +export const customWrapRoute: CustomWrapRoute = wrapRoute; + +// NOTE: We should not use layout or index routes in links + +export const rootLayout = customWrapRoute({ + path: '/', + errorElement: , + component: { + eagerLoad: true, + render: RootLayout, + props: {}, + }, + wrapperComponent: Auth, + context: { + title: 'IFRC Alert Hub', + visibility: 'anything', + }, +}); diff --git a/src/App/routes/index.tsx b/src/App/routes/index.tsx index e8d8987c..e194c249 100644 --- a/src/App/routes/index.tsx +++ b/src/App/routes/index.tsx @@ -6,11 +6,13 @@ import { MyOutputIndexRouteObject, MyOutputNonIndexRouteObject, unwrapRoute, - wrapRoute, } from '#utils/routes'; -import { Component as RootLayout } from '#views/RootLayout'; -import PageError from '../PageError'; +import Auth from '../Auth'; +import { + customWrapRoute, + rootLayout, +} from './common'; // NOTE: setting default ExtendedProps export type ExtendedProps = { @@ -27,22 +29,6 @@ export interface MyWrapRoute { ): MyOutputNonIndexRouteObject } -const customWrapRoute: MyWrapRoute = wrapRoute; - -const rootLayout = customWrapRoute({ - path: '/', - errorElement: , - component: { - render: RootLayout, - eagerLoad: true, - props: {}, - }, - context: { - title: 'IFRC Alert Hub', - visibility: 'anything', - }, -}); - type DefaultHomeChild = 'map'; const homeLayout = customWrapRoute({ parent: rootLayout, @@ -51,6 +37,7 @@ const homeLayout = customWrapRoute({ render: () => import('#views/Home'), props: {}, }, + wrapperComponent: Auth, context: { title: 'IFRC Alert Hub', visibility: 'anything', @@ -64,6 +51,7 @@ const mySubscription = customWrapRoute({ render: () => import('#views/MySubscription'), props: {}, }, + wrapperComponent: Auth, context: { title: 'My Subscriptions', // TODO: Change visibility after login feature @@ -82,6 +70,7 @@ const homeIndex = customWrapRoute({ replace: true, }, }, + wrapperComponent: Auth, context: { title: 'IFRC Alert Hub', visibility: 'anything', @@ -95,6 +84,7 @@ const homeMap = customWrapRoute({ render: () => import('#views/Home/AlertsMap'), props: {}, }, + wrapperComponent: Auth, context: { title: 'IFRC Alert Hub - Map', visibility: 'anything', @@ -108,6 +98,7 @@ const homeTable = customWrapRoute({ render: () => import('#views/Home/AlertsTable'), props: {}, }, + wrapperComponent: Auth, context: { title: 'IFRC Alert Hub - Table', visibility: 'anything', @@ -121,6 +112,7 @@ const preferences = customWrapRoute({ render: () => import('#views/Preferences'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Preferences', visibility: 'anything', @@ -134,6 +126,7 @@ const historicalAlerts = customWrapRoute({ render: () => import('#views/HistoricalAlerts'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Historical Alerts', visibility: 'anything', @@ -147,6 +140,7 @@ const about = customWrapRoute({ render: () => import('#views/About'), props: {}, }, + wrapperComponent: Auth, context: { title: 'About', visibility: 'anything', @@ -160,6 +154,7 @@ const resources = customWrapRoute({ render: () => import('#views/Resources'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Resources', visibility: 'anything', @@ -173,6 +168,7 @@ const alertDetails = customWrapRoute({ render: () => import('#views/AlertDetails'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Alert Details', visibility: 'anything', @@ -186,6 +182,7 @@ const allSourcesFeeds = customWrapRoute({ render: () => import('#views/AllSourcesFeeds'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Sources Feeds', visibility: 'anything', @@ -199,6 +196,7 @@ const pageNotFound = customWrapRoute({ render: () => import('#views/PageNotFound'), props: {}, }, + wrapperComponent: Auth, context: { title: '404', visibility: 'anything', @@ -212,6 +210,7 @@ const login = customWrapRoute({ render: () => import('#views/Login'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Login', visibility: 'is-not-authenticated', @@ -225,6 +224,7 @@ const recoverAccount = customWrapRoute({ render: () => import('#views/RecoverAccount'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Recover Account', visibility: 'is-not-authenticated', @@ -238,6 +238,7 @@ const resendValidationEmail = customWrapRoute({ render: () => import('#views/ResendValidationEmail'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Resend Validation Email', visibility: 'is-not-authenticated', @@ -251,6 +252,7 @@ const cookiePolicy = customWrapRoute({ render: () => import('#views/CookiePolicy'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Cookie Policy', visibility: 'anything', diff --git a/src/App/styles.module.css b/src/App/styles.module.css new file mode 100644 index 00000000..62cf9777 --- /dev/null +++ b/src/App/styles.module.css @@ -0,0 +1,7 @@ +.loading { + display: flex; + align-items: center; + justify-content: center; + width: 100vw; + height: 100vh; +} diff --git a/src/components/Link/index.tsx b/src/components/Link/index.tsx index b85ac36a..1f26d4c5 100644 --- a/src/components/Link/index.tsx +++ b/src/components/Link/index.tsx @@ -21,7 +21,7 @@ import { } from '@togglecorp/fujs'; import RouteContext from '#contexts/route'; -import useAuth from '#hooks/useAuth'; +import useAuth from '#hooks/domain/useAuth'; import { type WrappedRoutes } from '../../App/routes'; diff --git a/src/components/Navbar/i18n.json b/src/components/Navbar/i18n.json index 7ffcd233..117178a3 100644 --- a/src/components/Navbar/i18n.json +++ b/src/components/Navbar/i18n.json @@ -7,6 +7,8 @@ "appResources": "Resources", "headerMenuHome": "Home", "headerMenuMySubscription": "My Subscriptions", - "historicalAlerts": "Historical Alerts" + "historicalAlerts": "Historical Alerts", + "logoutFailure": "Failed to logout", + "userLogout":"Logout" } } diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index eb1ebc2b..06221e83 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -1,4 +1,10 @@ +import { useContext } from 'react'; import { + gql, + useMutation, +} from '@apollo/client'; +import { + Button, Heading, NavigationTabList, PageContainer, @@ -9,18 +15,67 @@ import { _cs } from '@togglecorp/fujs'; import goLogo from '#assets/icons/go-logo-2020.svg'; import Link from '#components/Link'; import NavigationTab from '#components/NavigationTab'; +import UserContext from '#contexts/user'; +import { LogoutMutation } from '#generated/types/graphql'; +import useAuth from '#hooks/domain/useAuth'; +import useAlert from '#hooks/useAlert'; import LangaugeDropdown from './LanguageDropdown'; import i18n from './i18n.json'; import styles from './styles.module.css'; +const LOGOUT = gql` + mutation Logout { + private { + logout { + ok + errors + } + } + } +`; + interface Props { className?: string; } function Navbar(props: Props) { const { className } = props; const strings = useTranslation(i18n); + const { isAuthenticated } = useAuth(); + const alert = useAlert(); + + const { + removeUserAuth: removeUser, + } = useContext(UserContext); + + const [ + triggerLogout, + { loading: logoutPending }, + ] = useMutation( + LOGOUT, + { + onCompleted: (logoutResponse) => { + const response = logoutResponse?.private?.logout; + if (response.ok) { + window.location.reload(); + removeUser(); + } else { + alert.show( + strings.logoutFailure, + { variant: 'danger' }, + ); + } + }, + onError: () => { + alert.show( + strings.logoutFailure, + { variant: 'danger' }, + ); + }, + }, + ); + return (
-& Pick -& { - alert: string | undefined; - startDateFrom: string | undefined; - startDateTo: string | undefined; -}; + & Pick + & { + alert: string | undefined; + startDateFrom: string | undefined; + startDateTo: string | undefined; + }; type CombinedAlertFilterKey = ApplicableAlertFilterKey | 'alert' | 'startDateFrom' | 'startDateTo'; const filterKeys: CombinedAlertFilterKey[] = ['country', 'admin1', 'region', 'urgency', 'severity', 'category', 'certainty', 'alert', 'startDateTo', 'startDateFrom']; @@ -92,8 +103,8 @@ export function Component() { admin1: convertUrlQueryToId(urlValues.admin1), region: convertUrlQueryToId(urlValues.region), infos: (isDefined(category) - || isDefined(urgency) - || isDefined(severity) || isDefined(certainty)) + || isDefined(urgency) + || isDefined(severity) || isDefined(certainty)) ? ({ category, urgency, @@ -214,6 +225,20 @@ export function Component() { getFilterFieldSetterFn, ], ); + const defaultSubscription = useMemo(() => ({ + filterAlertUrgencies: alertContextValue.selectedUrgencyTypes, + filterAlertCertainties: alertContextValue.selectedCertaintyTypes, + filterAlertSeverities: alertContextValue.selectedSeverityTypes, + filterAlertCategories: alertContextValue.selectedCategoryTypes, + filterAlertCountry: alertContextValue.activeCountryId, + filterAlertAdmin1s: alertContextValue.activeAdmin1Id + ? [alertContextValue.activeAdmin1Id] : [], + }), [alertContextValue]); + + const [showSubscriptionModal, { + setTrue: setShowSubscriptionModalTrue, + setFalse: setShowSubscriptionModalFalse, + }] = useBooleanState(false); return ( @@ -225,15 +250,93 @@ export function Component() { infoContainerClassName={styles.tabSection} mainSectionClassName={styles.content} info={( - - - {strings.mapTabTitle} - - - {strings.tableTabTitle} - - + <> + + + + {strings.alertNewSubscription} + + )} + /> + + + + + {showSubscriptionModal && ( + + )} + + + {strings.alertApiReference} + + )} + /> + + + + + +
+ + + {strings.mapTabTitle} + + + {strings.tableTabTitle} + + +
+ )} + > diff --git a/src/views/Home/styles.module.css b/src/views/Home/styles.module.css index f21f003f..13e65d9f 100644 --- a/src/views/Home/styles.module.css +++ b/src/views/Home/styles.module.css @@ -7,7 +7,32 @@ .tab-section { display: flex; + flex-direction: column; + gap:var(--go-ui-spacing-xl); justify-content: center; + + .cards { + gap: var(--go-ui-spacing-xl); + + .card { + gap: var(--go-ui-spacing-lg); + border-radius: var(--go-ui-border-radius-lg); + box-shadow: var(--go-ui-box-shadow-md); + background-color: var(--go-ui-color-foreground); + overflow: auto; + + .cards-content { + display:flex; + align-items: center; + + .alert-image { + margin: 0 auto; + object-fit: contain; + height: 5rem; + } + } + } + } } .map-filter, @@ -16,4 +41,4 @@ flex-direction: column; gap: var(--go-ui-spacing-lg); } -} \ No newline at end of file +} diff --git a/src/views/Login/i18n.json b/src/views/Login/i18n.json index 98a1f050..f06d9a11 100644 --- a/src/views/Login/i18n.json +++ b/src/views/Login/i18n.json @@ -16,7 +16,7 @@ "loginButton":"Login", "loginDontHaveAccount":"Don’t have an account? {signUpLink}", "loginCreateAccountTitle":"Create new account", - "loginSignUp":"Sign up", + "loginRegister":"Register", "loginFailureMessage": "Failed to login!", "loginSuccessfully": "Logged in successfully!" } diff --git a/src/views/Login/index.tsx b/src/views/Login/index.tsx index 4d296cd7..88845e11 100644 --- a/src/views/Login/index.tsx +++ b/src/views/Login/index.tsx @@ -161,15 +161,15 @@ export function Component() { validate, ]); - const signupInfo = resolveToComponent( + const registerInfo = resolveToComponent( strings.loginDontHaveAccount, { - signUpLink: ( + registerLink: ( - {strings.loginSignUp} + {strings.loginRegister} ), }, @@ -232,7 +232,7 @@ export function Component() { {strings.loginButton}
- {signupInfo} + {registerInfo}
From f9b12d5d01c9f7111362c705a0c829a1ec1f30ca Mon Sep 17 00:00:00 2001 From: roshni73 Date: Tue, 3 Dec 2024 14:08:28 +0545 Subject: [PATCH 21/30] Fix Historical alert filter ,table filter --- src/components/Navbar/index.tsx | 5 +- src/views/HistoricalAlerts/index.tsx | 69 ++++++++++++-------- src/views/HistoricalAlerts/styles.module.css | 10 +++ src/views/Home/AlertsTable/index.tsx | 9 +++ 4 files changed, 66 insertions(+), 27 deletions(-) diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index ca9ac0e5..121f8373 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -1,4 +1,5 @@ import { useContext } from 'react'; +import { useNavigate } from 'react-router-dom'; import { gql, useMutation, @@ -40,6 +41,7 @@ interface Props { className?: string; } function Navbar(props: Props) { + const navigate = useNavigate(); const { className } = props; const strings = useTranslation(i18n); const { isAuthenticated } = useAuth(); @@ -58,8 +60,9 @@ function Navbar(props: Props) { onCompleted: (logoutResponse) => { const response = logoutResponse?.private?.logout; if (response.ok) { - window.location.reload(); removeUser(); + navigate('/login'); + window.location.reload(); } else { alert.show( strings.logoutFailure, diff --git a/src/views/HistoricalAlerts/index.tsx b/src/views/HistoricalAlerts/index.tsx index b0d798fd..44902521 100644 --- a/src/views/HistoricalAlerts/index.tsx +++ b/src/views/HistoricalAlerts/index.tsx @@ -30,6 +30,7 @@ import { resolveToString, } from '@ifrc-go/ui/utils'; import { + doesObjectHaveNoData, isDefined, isNotDefined, } from '@togglecorp/fujs'; @@ -197,40 +198,55 @@ export function Component() { const variables = useMemo<{ filters: AlertFilter | undefined, pagination: OffsetPaginationInput, - }>(() => ({ - pagination: { - offset, - limit, - }, - filters: finalFilter ? { - DISTINCT: true, - infos: { - urgency: finalFilter?.urgency, - severity: finalFilter?.severity, - certainty: finalFilter?.certainty, - category: finalFilter?.category, + }>(() => { + const sentFilter = finalFilter?.startDateBefore || finalFilter?.startDateAfter ? { + range: { + ...(finalFilter?.startDateBefore && { end: finalFilter.startDateBefore }), + ...(finalFilter?.startDateAfter && { start: finalFilter.startDateAfter }), }, - country: isDefined(finalFilter?.country?.pk) - ? { pk: finalFilter.country.pk } : undefined, - admin1: finalFilter?.admin1, - sent: { - range: { - end: finalFilter?.startDateBefore, - start: finalFilter?.startDateAfter, - }, + } : undefined; + return { + pagination: { + offset, + limit, }, - } : undefined, - }), [ + filters: finalFilter ? { + DISTINCT: true, + infos: { + urgency: finalFilter?.urgency, + severity: finalFilter?.severity, + certainty: finalFilter?.certainty, + category: finalFilter?.category, + }, + country: isDefined(finalFilter?.country?.pk) + ? { pk: finalFilter.country.pk } : undefined, + admin1: finalFilter?.admin1, + sent: sentFilter, + } : undefined, + }; + }, [ limit, offset, finalFilter, ]); const handleApplyFilters = useCallback(() => { - setFinalFilter(rawFilter); - }, [ - rawFilter, - ]); + if (doesObjectHaveNoData(rawFilter)) { + setFinalFilter(undefined); + } else { + const updatedFilter = { + ...rawFilter, + sent: rawFilter.startDateBefore || rawFilter.startDateAfter ? { + range: { + ...(rawFilter.startDateBefore && { end: rawFilter.startDateBefore }), + ...(rawFilter.startDateAfter && { start: rawFilter.startDateAfter }), + }, + } : {}, + }; + setFinalFilter(updatedFilter); + } + setPage(1); + }, [rawFilter, setPage]); const handleResetFilters = useCallback(() => { setFinalFilter(undefined); @@ -401,6 +417,7 @@ export function Component() { errorMessage={alertInfoError?.message} footerActions={isDefined(data) && ( Date: Fri, 29 Nov 2024 14:42:47 +0545 Subject: [PATCH 22/30] feat: use hcaptcha environment variable during build --- Dockerfile | 1 + nginx-serve/apply-config.sh | 2 ++ nginx-serve/helm/templates/configmap.yaml | 1 + nginx-serve/helm/templates/deployment.yaml | 4 ++-- nginx-serve/helm/templates/service.yaml | 4 ++-- nginx-serve/helm/values-test.yaml | 1 + nginx-serve/helm/values.yaml | 1 + 7 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 96a0232b..6a0bc2a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,6 +33,7 @@ ENV APP_ENVIRONMENT=APP_ENVIRONMENT_PLACEHOLDER ENV APP_MAPBOX_ACCESS_TOKEN=APP_MAPBOX_ACCESS_TOKEN_PLACEHOLDER ENV APP_GOOGLE_ANALYTICS_ID=APP_GOOGLE_ANALYTICS_ID_PLACEHOLDER ENV APP_GRAPHQL_API_ENDPOINT=https://APP-GRAPHQL-API-ENDPOINT-PLACEHOLDER.COM/ +ENV APP_HCAPTCHA_SITEKEY=APP_HCAPTCHA_SITEKEY_PLACEHOLDER # Build variables (Requires backend pulled) ENV APP_GRAPHQL_CODEGEN_ENDPOINT=./backend/schema.graphql diff --git a/nginx-serve/apply-config.sh b/nginx-serve/apply-config.sh index c81214e1..11eec813 100755 --- a/nginx-serve/apply-config.sh +++ b/nginx-serve/apply-config.sh @@ -30,6 +30,8 @@ find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\|$APP_MAPBOX_ACCESS_TOKEN|g" {} + find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\|$APP_GOOGLE_ANALYTICS_ID|g" {} + find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\ Date: Tue, 3 Dec 2024 17:40:34 +0545 Subject: [PATCH 23/30] Fix subscriptions bugs --- src/hooks/useFilterState.ts | 17 ++++++++++++-- src/views/HistoricalAlerts/index.tsx | 8 ++++--- src/views/MySubscription/i18n.json | 2 +- src/views/MySubscription/index.tsx | 26 +++++++++------------- src/views/MySubscription/styles.module.css | 14 ++---------- src/views/NewSubscriptionModal/i18n.json | 1 + src/views/NewSubscriptionModal/index.tsx | 19 ++++++++-------- 7 files changed, 45 insertions(+), 42 deletions(-) diff --git a/src/hooks/useFilterState.ts b/src/hooks/useFilterState.ts index 1d0be26d..b11ef193 100644 --- a/src/hooks/useFilterState.ts +++ b/src/hooks/useFilterState.ts @@ -6,11 +6,10 @@ import { } from 'react'; import { hasSomeDefinedValue } from '@ifrc-go/ui/utils'; import { isNotDefined } from '@togglecorp/fujs'; +import { EntriesAsList } from '@togglecorp/toggle-form'; import useDebouncedValue from '#hooks/useDebouncedValue'; -import { EntriesAsList } from '../types'; - type SortDirection = 'asc' | 'dsc'; interface SortParameter { name: string; @@ -135,6 +134,13 @@ function useFilterState(options: { [], ); + const resetFilter = useCallback( + () => { + dispatch({ type: 'reset-filter' }); + }, + [], + ); + const setFilterField = useCallback( (...args: EntriesAsList) => { const [val, key] = args; @@ -182,15 +188,22 @@ function useFilterState(options: { () => hasSomeDefinedValue(debouncedState.filter), [debouncedState.filter], ); + const rawFiltered = useMemo( + () => hasSomeDefinedValue(state.filter), + [state.filter], + ); return { rawFilter: state.filter, + rawFiltered, filter: debouncedState.filter, filtered, setFilter, setFilterField, + resetFilter, + page: state.page, offset: pageSize * (debouncedState.page - 1), limit: pageSize, diff --git a/src/views/HistoricalAlerts/index.tsx b/src/views/HistoricalAlerts/index.tsx index 44902521..3c250d3a 100644 --- a/src/views/HistoricalAlerts/index.tsx +++ b/src/views/HistoricalAlerts/index.tsx @@ -189,7 +189,7 @@ export function Component() { setFilterField, filtered, offset, - setFilter, + resetFilter, } = useFilterState({ pageSize: PAGE_SIZE, filter: {}, @@ -249,9 +249,11 @@ export function Component() { }, [rawFilter, setPage]); const handleResetFilters = useCallback(() => { + resetFilter(); setFinalFilter(undefined); - setFilter({}); - }, [setFilter]); + }, [ + resetFilter, + ]); const { loading: alertInfoLoading, diff --git a/src/views/MySubscription/i18n.json b/src/views/MySubscription/i18n.json index 52fb5282..2c6640e7 100644 --- a/src/views/MySubscription/i18n.json +++ b/src/views/MySubscription/i18n.json @@ -6,7 +6,7 @@ "createNewSubscription": "Create", "sendViaEmailLabel": "Send via email", "activeSubscriptionsTab": "Active Subscriptions", - "archivedSubscriptionTab": "Archive Subscriptions", + "archivedSubscriptionTab": "Archived Subscriptions", "subscriptionUnarchived": "Subscription unarchived.", "subscriptionArchived": "Subscription archived.", "subscriptionFailedToUpdate": "Failed to update subscription.", diff --git a/src/views/MySubscription/index.tsx b/src/views/MySubscription/index.tsx index 5b456efb..453dee45 100644 --- a/src/views/MySubscription/index.tsx +++ b/src/views/MySubscription/index.tsx @@ -12,8 +12,8 @@ import { AddLineIcon } from '@ifrc-go/icons'; import { Button, Container, - List, Pager, + RawList, Tab, TabList, TabPanel, @@ -311,8 +311,9 @@ export function Component() { name: selectedSubscriptionDetails?.name ?? '', }, }, + }).then(() => { + refetch(); }); - refetch(); }, [ data?.items, triggerSubscriptionUpdate, @@ -396,10 +397,10 @@ export function Component() { className={styles.mySubscription} heading={strings.mySubscription} description={strings.subscriptionDescription} - mainSectionClassName={styles.content} > )} + pending={alertSubscriptionLoading} + errored={isDefined(alertSubscriptionError)} + overlayPending > {showSubscriptionModal && ( - - diff --git a/src/views/MySubscription/styles.module.css b/src/views/MySubscription/styles.module.css index 663c717b..78857ec0 100644 --- a/src/views/MySubscription/styles.module.css +++ b/src/views/MySubscription/styles.module.css @@ -1,15 +1,5 @@ .mySubscription { - .content { - display: flex; - flex-direction: column; - gap: var(--go-ui-spacing-2xl); - - .subscriptions { - .subscription { - display: flex; - flex-direction: column; - gap: var(--go-ui-spacing-lg); - } - } + .tabPanel { + display: contents; } } \ No newline at end of file diff --git a/src/views/NewSubscriptionModal/i18n.json b/src/views/NewSubscriptionModal/i18n.json index 69bbd6e9..3236f300 100644 --- a/src/views/NewSubscriptionModal/i18n.json +++ b/src/views/NewSubscriptionModal/i18n.json @@ -21,6 +21,7 @@ "newSubscriptionTitle": "Title", "newSubscriptionCreatedSucessfully": "Subscription created successfully.", "newSubscriptionFailed": "Failed to create subscription", + "newSubscriptionLimitExceed": "You have reached the maximum limit of 10 subscriptions", "subscriptionUpdatedSuccessfully": "Subscription updated successfully.", "failedToUpdateSubscription": "Failed to update subscription." } diff --git a/src/views/NewSubscriptionModal/index.tsx b/src/views/NewSubscriptionModal/index.tsx index 76e02fe5..f02342c1 100644 --- a/src/views/NewSubscriptionModal/index.tsx +++ b/src/views/NewSubscriptionModal/index.tsx @@ -214,10 +214,8 @@ const categoryLabelSelector = (category: Category) => category.label; type PartialFormFields = PartialForm; type FormSchema = ObjectSchema; -type FormSchemaFields = ReturnType - const formSchema: FormSchema = { - fields: (): FormSchemaFields => ({ + fields: (value) => ({ name: { required: true, requiredValidation: requiredStringCondition, @@ -245,8 +243,8 @@ const formSchema: FormSchema = { requiredValidation: requiredCondition, }, emailFrequency: { - required: true, - requiredValidation: requiredCondition, + required: !!value?.notifyByEmail, + requiredValidation: value?.notifyByEmail ? requiredCondition : undefined, }, }), }; @@ -421,6 +419,12 @@ function NewSubscriptionModal(props: Props) { subscriptionId: subscription.id, data: val as UserAlertSubscriptionInput, }, + }).then(() => { + if (onSuccess) { + ( + onSuccess() + ); + } }); } else { createAlertSubscription({ @@ -434,9 +438,6 @@ function NewSubscriptionModal(props: Props) { } }, ); - if (onSuccess) { - onSuccess(); - } handler(); }, [ setError, @@ -560,7 +561,7 @@ function NewSubscriptionModal(props: Props) { labelSelector={frequencyLabelSelector} value={value?.emailFrequency} onChange={setFieldValue} - disabled={isNotDefined(value.notifyByEmail)} + disabled={!value.notifyByEmail} error={fieldError?.emailFrequency} /> From edbca2067e10b1d19f50af7c44dbcf434e87ac0e Mon Sep 17 00:00:00 2001 From: barshathakuri Date: Thu, 1 Aug 2024 16:25:02 +0545 Subject: [PATCH 24/30] Add env to docker compose file --- docker-compose-prod.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index f1415f1e..7af4362a 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -101,6 +101,8 @@ services: APP_GRAPHQL_API_ENDPOINT: ${FRONTEND_APP_GRAPHQL_API_ENDPOINT?error} APP_MAPBOX_ACCESS_TOKEN: ${FRONTEND_APP_MAPBOX_ACCESS_TOKEN?error} APP_GRAPHQL_CODEGEN_ENDPOINT: ${FRONTEND_APP_GRAPHQL_CODEGEN_ENDPOINT?error} + env_file: + - .env command: | sh -c 'pnpm generate && pnpm build && rm -rf /client-build/* ; cp -r build/* /client-build/' volumes: From 02ecb406ca52fc77a9e2dc9c61e6fc1650349a40 Mon Sep 17 00:00:00 2001 From: roshni73 Date: Wed, 4 Dec 2024 14:48:07 +0545 Subject: [PATCH 25/30] fix register link --- src/App/routes/index.tsx | 1 + src/views/Login/i18n.json | 2 +- src/views/Login/index.tsx | 2 +- src/views/Login/styles.module.css | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/App/routes/index.tsx b/src/App/routes/index.tsx index dd24ba4a..643b87b7 100644 --- a/src/App/routes/index.tsx +++ b/src/App/routes/index.tsx @@ -65,6 +65,7 @@ const subscriptionDetail = customWrapRoute({ render: () => import('#views/MySubscription/SubscriptionDetail'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Subscription Detail', visibility: 'is-authenticated', diff --git a/src/views/Login/i18n.json b/src/views/Login/i18n.json index f06d9a11..8fe03e0f 100644 --- a/src/views/Login/i18n.json +++ b/src/views/Login/i18n.json @@ -14,7 +14,7 @@ "loginInvalid":"Invalid username or password", "loginErrorMessage":"Error: {message}", "loginButton":"Login", - "loginDontHaveAccount":"Don’t have an account? {signUpLink}", + "loginDontHaveAccount":"Don’t have an account? {registerLink}", "loginCreateAccountTitle":"Create new account", "loginRegister":"Register", "loginFailureMessage": "Failed to login!", diff --git a/src/views/Login/index.tsx b/src/views/Login/index.tsx index 88845e11..7a857eb2 100644 --- a/src/views/Login/index.tsx +++ b/src/views/Login/index.tsx @@ -231,7 +231,7 @@ export function Component() { > {strings.loginButton} -
+
{registerInfo}
diff --git a/src/views/Login/styles.module.css b/src/views/Login/styles.module.css index f13ba8b8..823b2c65 100644 --- a/src/views/Login/styles.module.css +++ b/src/views/Login/styles.module.css @@ -29,7 +29,7 @@ gap: var(--go-ui-spacing-lg); align-items: center; - .sign-up { + .register { display: flex; gap: var(--go-ui-spacing-sm); } From 86b45b9e6c440f98df530925a2a39666535e4f73 Mon Sep 17 00:00:00 2001 From: barshathakuri Date: Thu, 5 Dec 2024 17:43:25 +0545 Subject: [PATCH 26/30] Fix subscription styling --- backend | 2 +- package.json | 7 +- patches/@ifrc-go__ui@1.2.1.patch | 47 ++++++ pnpm-lock.yaml | 10 +- src/App/routes/index.tsx | 8 +- src/components/Navbar/index.tsx | 2 +- src/views/Home/styles.module.css | 7 +- .../ArchiveTableActions/i18n.json | 7 - .../AlertInfoItem/styles.module.css | 4 - .../SubscriptionDetail/i18n.json | 10 -- .../SubscriptionDetail/styles.module.css | 24 --- .../SubscriptionTableItem/styles.module.css | 15 -- .../ActiveTableActions/i18n.json | 3 +- .../ActiveTableActions/index.tsx | 14 +- .../ArchiveTableActions/i18n.json | 8 + .../ArchiveTableActions/index.tsx | 14 +- .../AlertInfoItem/i18n.json | 0 .../AlertInfoItem/index.tsx | 2 + .../AlertInfoItem/styles.module.css | 4 + .../SubscriptionDetail/i18n.json | 17 ++ .../SubscriptionDetail/index.tsx | 145 +++++++++++------- .../SubscriptionDetail/styles.module.css | 11 ++ .../SubscriptionTableItem/i18n.json | 0 .../SubscriptionTableItem/index.tsx | 97 ++++++++---- .../SubscriptionTableItem/styles.module.css | 14 ++ .../i18n.json | 8 +- .../index.tsx | 47 +++--- .../styles.module.css | 0 src/views/NewSubscriptionModal/i18n.json | 6 +- src/views/NewSubscriptionModal/index.tsx | 11 +- 30 files changed, 344 insertions(+), 200 deletions(-) create mode 100644 patches/@ifrc-go__ui@1.2.1.patch delete mode 100644 src/views/MySubscription/ArchiveTableActions/i18n.json delete mode 100644 src/views/MySubscription/SubscriptionDetail/AlertInfoItem/styles.module.css delete mode 100644 src/views/MySubscription/SubscriptionDetail/i18n.json delete mode 100644 src/views/MySubscription/SubscriptionDetail/styles.module.css delete mode 100644 src/views/MySubscription/SubscriptionTableItem/styles.module.css rename src/views/{MySubscription => MySubscriptions}/ActiveTableActions/i18n.json (57%) rename src/views/{MySubscription => MySubscriptions}/ActiveTableActions/index.tsx (80%) create mode 100644 src/views/MySubscriptions/ArchiveTableActions/i18n.json rename src/views/{MySubscription => MySubscriptions}/ArchiveTableActions/index.tsx (76%) rename src/views/{MySubscription => MySubscriptions}/SubscriptionDetail/AlertInfoItem/i18n.json (100%) rename src/views/{MySubscription => MySubscriptions}/SubscriptionDetail/AlertInfoItem/index.tsx (91%) create mode 100644 src/views/MySubscriptions/SubscriptionDetail/AlertInfoItem/styles.module.css create mode 100644 src/views/MySubscriptions/SubscriptionDetail/i18n.json rename src/views/{MySubscription => MySubscriptions}/SubscriptionDetail/index.tsx (59%) create mode 100644 src/views/MySubscriptions/SubscriptionDetail/styles.module.css rename src/views/{MySubscription => MySubscriptions}/SubscriptionTableItem/i18n.json (100%) rename src/views/{MySubscription => MySubscriptions}/SubscriptionTableItem/index.tsx (57%) create mode 100644 src/views/MySubscriptions/SubscriptionTableItem/styles.module.css rename src/views/{MySubscription => MySubscriptions}/i18n.json (83%) rename src/views/{MySubscription => MySubscriptions}/index.tsx (91%) rename src/views/{MySubscription => MySubscriptions}/styles.module.css (100%) diff --git a/backend b/backend index 5c673b26..a06277e3 160000 --- a/backend +++ b/backend @@ -1 +1 @@ -Subproject commit 5c673b2604105ecb97a4c401a477b089d45c77ca +Subproject commit a06277e38cdc2922a9673decc6c83ca16837393e diff --git a/package.json b/package.json index 1a3df5c0..a0ae8148 100644 --- a/package.json +++ b/package.json @@ -94,5 +94,10 @@ "vite-tsconfig-paths": "^4.2.2", "vitest": "^1.1.0" }, - "packageManager": "pnpm@8.6.0+sha1.71f9126a20cd3d00fa47c188f956918858180e54" + "packageManager": "pnpm@8.6.0+sha1.71f9126a20cd3d00fa47c188f956918858180e54", + "pnpm": { + "patchedDependencies": { + "@ifrc-go/ui@1.2.1": "patches/@ifrc-go__ui@1.2.1.patch" + } + } } diff --git a/patches/@ifrc-go__ui@1.2.1.patch b/patches/@ifrc-go__ui@1.2.1.patch new file mode 100644 index 00000000..3393a2dd --- /dev/null +++ b/patches/@ifrc-go__ui@1.2.1.patch @@ -0,0 +1,47 @@ +diff --git a/dist/components/Chip/index.d.ts b/dist/components/Chip/index.d.ts +index 7345249eaad9a8fb959fe0330639292354032909..ceba3ed438c7fa0b687d07f7591bab624aff06bc 100644 +--- a/dist/components/Chip/index.d.ts ++++ b/dist/components/Chip/index.d.ts +@@ -2,7 +2,7 @@ export type ChipVariant = 'primary' | 'secondary' | 'tertiary'; + export interface Props { + className?: string; + name: N; +- label: string; ++ label: React.ReactNode; + variant?: ChipVariant; + onDelete?: (name: N, e: React.MouseEvent) => void; + } +diff --git a/package.json b/package.json +index 999e201527e87de45a377344ca40f13212549f61..8ce9c9c085e950405fb0d0f888d5c57876b5e080 100644 +--- a/package.json ++++ b/package.json +@@ -97,5 +97,10 @@ + "vite-plugin-lib-inject-css": "^1.3.0", + "vite-tsconfig-paths": "^4.2.3", + "vitest": "^1.1.1" ++ }, ++ "pnpm": { ++ "patchedDependencies": { ++ "@ifrc-go/ui@1.2.1": "patches/@ifrc-go__ui@1.2.1.patch" ++ } + } + } +diff --git a/patches/@ifrc-go__ui@1.2.1.patch b/patches/@ifrc-go__ui@1.2.1.patch +new file mode 100644 +index 0000000000000000000000000000000000000000..31603365255a6e1aa30c140c96b27fa3e93cd7fa +--- /dev/null ++++ b/patches/@ifrc-go__ui@1.2.1.patch +@@ -0,0 +1,13 @@ ++diff --git a/dist/components/Chip/index.d.ts b/dist/components/Chip/index.d.ts ++index 7345249eaad9a8fb959fe0330639292354032909..ceba3ed438c7fa0b687d07f7591bab624aff06bc 100644 ++--- a/dist/components/Chip/index.d.ts +++++ b/dist/components/Chip/index.d.ts ++@@ -2,7 +2,7 @@ export type ChipVariant = 'primary' | 'secondary' | 'tertiary'; ++ export interface Props { ++ className?: string; ++ name: N; ++- label: string; +++ label: React.ReactNode; ++ variant?: ChipVariant; ++ onDelete?: (name: N, e: React.MouseEvent) => void; ++ } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index def82ad0..da3f59fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,11 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +patchedDependencies: + '@ifrc-go/ui@1.2.1': + hash: c344e4kpykmuqcoldcef7qfjaq + path: patches/@ifrc-go__ui@1.2.1.patch + dependencies: '@apollo/client': specifier: ^3.9.9 @@ -16,7 +21,7 @@ dependencies: version: 1.3.3(react@18.2.0) '@ifrc-go/ui': specifier: ^1.2.1 - version: 1.2.1(@ifrc-go/icons@1.3.3)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + version: 1.2.1(patch_hash=c344e4kpykmuqcoldcef7qfjaq)(@ifrc-go/icons@1.3.3)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) '@mapbox/mapbox-gl-draw': specifier: ^1.4.3 version: 1.4.3 @@ -2429,7 +2434,7 @@ packages: react: 18.2.0 dev: false - /@ifrc-go/ui@1.2.1(@ifrc-go/icons@1.3.3)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): + /@ifrc-go/ui@1.2.1(patch_hash=c344e4kpykmuqcoldcef7qfjaq)(@ifrc-go/icons@1.3.3)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-ZdWffxnowjtY8wgIuheS4mf3P1JnoKKaEC8yrf0JHceb3fCenMXJqMBQEdRMaJNBtd4MhQhuls53uErzX/l2gA==} peerDependencies: '@ifrc-go/icons': ^1.3.1 @@ -2446,6 +2451,7 @@ packages: transitivePeerDependencies: - '@types/react' dev: false + patched: true /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} diff --git a/src/App/routes/index.tsx b/src/App/routes/index.tsx index 643b87b7..e1f22676 100644 --- a/src/App/routes/index.tsx +++ b/src/App/routes/index.tsx @@ -44,11 +44,11 @@ const homeLayout = customWrapRoute({ }, }); -const mySubscription = customWrapRoute({ +const mySubscriptions = customWrapRoute({ parent: rootLayout, path: 'subscriptions', component: { - render: () => import('#views/MySubscription'), + render: () => import('#views/MySubscriptions'), props: {}, }, wrapperComponent: Auth, @@ -62,7 +62,7 @@ const subscriptionDetail = customWrapRoute({ parent: rootLayout, path: 'subscriptions/:subscriptionId', component: { - render: () => import('#views/MySubscription/SubscriptionDetail'), + render: () => import('#views/MySubscriptions/SubscriptionDetail'), props: {}, }, wrapperComponent: Auth, @@ -354,7 +354,7 @@ const wrappedRoutes = { login, recoverAccount, // resendValidationEmail, - mySubscription, + mySubscriptions, cookiePolicy, register, historicalAlerts, diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index 121f8373..36a2353e 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -158,7 +158,7 @@ function Navbar(props: Props) { {isAuthenticated && ( {strings.headerMenuMySubscription} diff --git a/src/views/Home/styles.module.css b/src/views/Home/styles.module.css index 13e65d9f..1db31cba 100644 --- a/src/views/Home/styles.module.css +++ b/src/views/Home/styles.module.css @@ -8,7 +8,7 @@ .tab-section { display: flex; flex-direction: column; - gap:var(--go-ui-spacing-xl); + gap: var(--go-ui-spacing-xl); justify-content: center; .cards { @@ -22,8 +22,9 @@ overflow: auto; .cards-content { - display:flex; + display: flex; align-items: center; + justify-content: space-between; .alert-image { margin: 0 auto; @@ -41,4 +42,4 @@ flex-direction: column; gap: var(--go-ui-spacing-lg); } -} +} \ No newline at end of file diff --git a/src/views/MySubscription/ArchiveTableActions/i18n.json b/src/views/MySubscription/ArchiveTableActions/i18n.json deleted file mode 100644 index cd2fa016..00000000 --- a/src/views/MySubscription/ArchiveTableActions/i18n.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "namespace": "SubscriptionActions", - "strings": { - "unarchiveSubscriptionActions": "Unarchive", - "deleteSubscriptionActions": "Delete" - } -} diff --git a/src/views/MySubscription/SubscriptionDetail/AlertInfoItem/styles.module.css b/src/views/MySubscription/SubscriptionDetail/AlertInfoItem/styles.module.css deleted file mode 100644 index d335e76b..00000000 --- a/src/views/MySubscription/SubscriptionDetail/AlertInfoItem/styles.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.alert-detail { - background-color: var(--go-ui-color-gray-20); - padding: var(--go-ui-spacing-md); -} diff --git a/src/views/MySubscription/SubscriptionDetail/i18n.json b/src/views/MySubscription/SubscriptionDetail/i18n.json deleted file mode 100644 index c8c62571..00000000 --- a/src/views/MySubscription/SubscriptionDetail/i18n.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "namespace": "subscriptionDetail", - "strings": { - "subscriptionDetailTitle": "Subscription Detail", - "subscriptionHeading": "Subscription", - "filterStartDateFrom": "Start date from", - "filterStartDateTo": "Start date To", - "filterEmptyMessage": "Alerts not available!" - } -} \ No newline at end of file diff --git a/src/views/MySubscription/SubscriptionDetail/styles.module.css b/src/views/MySubscription/SubscriptionDetail/styles.module.css deleted file mode 100644 index b24803bf..00000000 --- a/src/views/MySubscription/SubscriptionDetail/styles.module.css +++ /dev/null @@ -1,24 +0,0 @@ -.subscription-detail { - .alert-filters { - display: flex; - flex-direction: column; - gap: var(--go-ui-spacing-md); - - .filters { - display: flex; - gap: var(--go-ui-spacing-md); - - .badge-container { - display: flex; - gap: var(--go-ui-spacing-md); - min-height: 0%; - } - } - - .alert-item { - display: flex; - flex-direction: column; - gap: var(--go-ui-spacing-md); - } - } -} \ No newline at end of file diff --git a/src/views/MySubscription/SubscriptionTableItem/styles.module.css b/src/views/MySubscription/SubscriptionTableItem/styles.module.css deleted file mode 100644 index 696bca66..00000000 --- a/src/views/MySubscription/SubscriptionTableItem/styles.module.css +++ /dev/null @@ -1,15 +0,0 @@ -.subscription-detail { - background-color: var(--go-ui-color-gray-20); - padding: var(--go-ui-spacing-md); - - .alertDetail { - display: flex; - flex-wrap: wrap; - gap: var(--go-ui-spacing-xl); - } - - .label { - display: flex; - flex-direction: column; - } -} \ No newline at end of file diff --git a/src/views/MySubscription/ActiveTableActions/i18n.json b/src/views/MySubscriptions/ActiveTableActions/i18n.json similarity index 57% rename from src/views/MySubscription/ActiveTableActions/i18n.json rename to src/views/MySubscriptions/ActiveTableActions/i18n.json index 57399421..3d6053d0 100644 --- a/src/views/MySubscription/ActiveTableActions/i18n.json +++ b/src/views/MySubscriptions/ActiveTableActions/i18n.json @@ -3,6 +3,7 @@ "strings": { "deleteSubscriptionActions": "Delete", "archiveSubscriptionActions": "Archive", - "editSubscriptionActions": "Edit" + "editSubscriptionActions": "Edit", + "confirmationMessage": "Are you sure want to delete the subscription?" } } diff --git a/src/views/MySubscription/ActiveTableActions/index.tsx b/src/views/MySubscriptions/ActiveTableActions/index.tsx similarity index 80% rename from src/views/MySubscription/ActiveTableActions/index.tsx rename to src/views/MySubscriptions/ActiveTableActions/index.tsx index 015c37a6..a573690a 100644 --- a/src/views/MySubscription/ActiveTableActions/index.tsx +++ b/src/views/MySubscriptions/ActiveTableActions/index.tsx @@ -1,3 +1,4 @@ +import { useCallback } from 'react'; import { DeleteBinSixLineIcon, EditTwoLineIcon, @@ -12,7 +13,7 @@ import DropdownMenuItem from '#components/DropdownMenuItem'; import i18n from './i18n.json'; interface Props { - onSubscriptionRemove?: () => void; + onSubscriptionRemove: () => void; onArchiveClick?: () => void; onEditClick: () => void; } @@ -26,11 +27,16 @@ function ActiveTableActions(props: Props) { const strings = useTranslation(i18n); + const handleDelete = useCallback(() => { + onSubscriptionRemove(); + }, [onSubscriptionRemove]); + return ( } variant="tertiary" withoutDropdownIcon + persistent > } + persist > {strings.deleteSubscriptionActions} diff --git a/src/views/MySubscriptions/ArchiveTableActions/i18n.json b/src/views/MySubscriptions/ArchiveTableActions/i18n.json new file mode 100644 index 00000000..b2a3c552 --- /dev/null +++ b/src/views/MySubscriptions/ArchiveTableActions/i18n.json @@ -0,0 +1,8 @@ +{ + "namespace": "SubscriptionActions", + "strings": { + "unarchiveSubscriptionActions": "Unarchive", + "deleteSubscriptionActions": "Delete", + "confirmationMessage": "Are you sure want to delete the subscription?" + } +} \ No newline at end of file diff --git a/src/views/MySubscription/ArchiveTableActions/index.tsx b/src/views/MySubscriptions/ArchiveTableActions/index.tsx similarity index 76% rename from src/views/MySubscription/ArchiveTableActions/index.tsx rename to src/views/MySubscriptions/ArchiveTableActions/index.tsx index 88e4ff6a..1737c520 100644 --- a/src/views/MySubscription/ArchiveTableActions/index.tsx +++ b/src/views/MySubscriptions/ArchiveTableActions/index.tsx @@ -1,3 +1,4 @@ +import { useCallback } from 'react'; import { DeleteBinSixLineIcon, LayoutBottomLineIcon, @@ -11,7 +12,7 @@ import DropdownMenuItem from '#components/DropdownMenuItem'; import i18n from './i18n.json'; interface Props { - onSubscriptionRemove?: () => void; + onSubscriptionRemove: () => void; onUnArchive?: () => void; } @@ -23,11 +24,16 @@ function ArchiveTableActions(props: Props) { const strings = useTranslation(i18n); + const handleDelete = useCallback(() => { + onSubscriptionRemove(); + }, [onSubscriptionRemove]); + return ( } variant="tertiary" withoutDropdownIcon + persistent > } + persist > {strings.deleteSubscriptionActions} diff --git a/src/views/MySubscription/SubscriptionDetail/AlertInfoItem/i18n.json b/src/views/MySubscriptions/SubscriptionDetail/AlertInfoItem/i18n.json similarity index 100% rename from src/views/MySubscription/SubscriptionDetail/AlertInfoItem/i18n.json rename to src/views/MySubscriptions/SubscriptionDetail/AlertInfoItem/i18n.json diff --git a/src/views/MySubscription/SubscriptionDetail/AlertInfoItem/index.tsx b/src/views/MySubscriptions/SubscriptionDetail/AlertInfoItem/index.tsx similarity index 91% rename from src/views/MySubscription/SubscriptionDetail/AlertInfoItem/index.tsx rename to src/views/MySubscriptions/SubscriptionDetail/AlertInfoItem/index.tsx index ec546bdd..6a191f68 100644 --- a/src/views/MySubscription/SubscriptionDetail/AlertInfoItem/index.tsx +++ b/src/views/MySubscriptions/SubscriptionDetail/AlertInfoItem/index.tsx @@ -26,6 +26,8 @@ function AlertInfoItem(props: Props) { className={styles.alertDetail} heading={alertTitle} headingLevel={4} + withInternalPadding + childrenContainerClassName={styles.content} actions={( )} - footerActions={( + footerActions={isDefined(alertsData) && ( )} + pending={alertLoading} + errored={isDefined(alertError)} + overlayPending >
- {isDefined(alertsData?.filterAlertCountry) && ( - - )} - {isDefined(alertsData?.filterAlertAdmin1sDisplay) - && alertsData.filterAlertAdmin1sDisplay?.map((admin) => ( - - ))} - {isDefined(alertsData?.filterAlertUrgenciesDisplay) - && alertsData.filterAlertUrgenciesDisplay?.map((urgency) => ( - + admin.name, + ).join(', ')} + strongLabel /> - ))} - {isDefined(alertsData?.filterAlertCertaintiesDisplay) - && alertsData.filterAlertCertaintiesDisplay?.map((certainty) => ( - + - ))} - {isDefined(alertsData?.filterAlertCategoriesDisplay) - && alertsData.filterAlertCategoriesDisplay?.map((category) => ( - + - ))} - {isDefined(alertsData?.filterAlertSeveritiesDisplay) - && alertsData.filterAlertSeveritiesDisplay?.map((severity) => ( - + - ))} + )} + /> + + )} + />
- + + +
); diff --git a/src/views/MySubscriptions/SubscriptionDetail/styles.module.css b/src/views/MySubscriptions/SubscriptionDetail/styles.module.css new file mode 100644 index 00000000..3d9333d8 --- /dev/null +++ b/src/views/MySubscriptions/SubscriptionDetail/styles.module.css @@ -0,0 +1,11 @@ +.subscription-detail { + .filters { + display: flex; + flex-wrap: wrap; + gap: var(--go-ui-spacing-xs); + + .filter-item { + font-size: var(--go-ui-font-size-xs); + } + } +} \ No newline at end of file diff --git a/src/views/MySubscription/SubscriptionTableItem/i18n.json b/src/views/MySubscriptions/SubscriptionTableItem/i18n.json similarity index 100% rename from src/views/MySubscription/SubscriptionTableItem/i18n.json rename to src/views/MySubscriptions/SubscriptionTableItem/i18n.json diff --git a/src/views/MySubscription/SubscriptionTableItem/index.tsx b/src/views/MySubscriptions/SubscriptionTableItem/index.tsx similarity index 57% rename from src/views/MySubscription/SubscriptionTableItem/index.tsx rename to src/views/MySubscriptions/SubscriptionTableItem/index.tsx index e08b2aa3..0c8d7118 100644 --- a/src/views/MySubscription/SubscriptionTableItem/index.tsx +++ b/src/views/MySubscriptions/SubscriptionTableItem/index.tsx @@ -1,5 +1,7 @@ import { + Chip, Container, + NumberOutput, TextOutput, } from '@ifrc-go/ui'; import { useTranslation } from '@ifrc-go/ui/hooks'; @@ -40,77 +42,106 @@ function SubscriptionTableItem(props: Props) { return ( + )} actions={( <> - ( - {alertCount} - ) + + {strings.subscriptionItemView} + {actions} )} - footerContentClassName={styles.alertDetail} - footerContent={( - <> + childrenContainerClassName={styles.content} + > + + )} + /> + + )} + /> + + )} + /> + + )} + /> + + )} + /> + - - )} - footerActions={( - - {strings.subscriptionItemView} - - )} - /> + )} + /> + ); } diff --git a/src/views/MySubscriptions/SubscriptionTableItem/styles.module.css b/src/views/MySubscriptions/SubscriptionTableItem/styles.module.css new file mode 100644 index 00000000..88054597 --- /dev/null +++ b/src/views/MySubscriptions/SubscriptionTableItem/styles.module.css @@ -0,0 +1,14 @@ +.subscription-detail { + border-radius: var(--go-ui-border-radius-lg); + background-color: var(--go-ui-color-background); + + .content { + display: flex; + flex-wrap: wrap; + gap: var(--go-ui-spacing-xs); + + .filter-item { + font-size: var(--go-ui-font-size-xs); + } + } +} diff --git a/src/views/MySubscription/i18n.json b/src/views/MySubscriptions/i18n.json similarity index 83% rename from src/views/MySubscription/i18n.json rename to src/views/MySubscriptions/i18n.json index 2c6640e7..5ce22e1c 100644 --- a/src/views/MySubscription/i18n.json +++ b/src/views/MySubscriptions/i18n.json @@ -1,7 +1,7 @@ { - "namespace": "mySubscription", + "namespace": "mySubscriptions", "strings": { - "mySubscription": "My Subscription", + "mySubscription": "My Subscriptions", "myNewSubscription": "New Subscription", "createNewSubscription": "Create", "sendViaEmailLabel": "Send via email", @@ -12,7 +12,7 @@ "subscriptionFailedToUpdate": "Failed to update subscription.", "subscriptionDescription": "Customize your alerts with tailored filters, manage your subscriptions, and stay informed with relevant alerts delivered directly to your inbox.", "subscriptionDeleted": "Subscription deleted.", - "subscriptionFailedToDelete": "Failed to delete subscription" - + "subscriptionFailedToDelete": "Failed to delete subscription", + "subscriptionEmptyMessage": "No subscription available!" } } diff --git a/src/views/MySubscription/index.tsx b/src/views/MySubscriptions/index.tsx similarity index 91% rename from src/views/MySubscription/index.tsx rename to src/views/MySubscriptions/index.tsx index 453dee45..6d81b0ec 100644 --- a/src/views/MySubscription/index.tsx +++ b/src/views/MySubscriptions/index.tsx @@ -65,11 +65,9 @@ const ALERT_SUBSCRIPTIONS = gql` name isActive notifyByEmail - alerts { - count - } emailFrequency emailFrequencyDisplay + totalAlertsCount filterAlertAdmin1s filterAlertAdmin1sDisplay { id @@ -282,8 +280,9 @@ export function Component() { variables: { subscriptionId: id, }, + }).then(() => { + refetch(); }); - refetch(); }, [ triggerSubscriptionDelete, refetch, @@ -326,7 +325,7 @@ export function Component() { ) => ({ id: value.id, name: value.name, - alertCount: value.alerts.count ?? 0, + alertCount: value.totalAlertsCount ?? 0, filterAlertUrgencies: value?.filterAlertUrgenciesDisplay, filterAlertCertainties: value?.filterAlertCertaintiesDisplay, filterAlertSeverities: value?.filterAlertSeveritiesDisplay, @@ -350,7 +349,7 @@ export function Component() { const archiveRendererParams = useCallback((_: string, value: UserAlertSubscriptionType) => ({ id: value.id, name: value.name, - alertCount: value.alerts.count ?? 0, + alertCount: value.totalAlertsCount ?? 0, filterAlertUrgencies: value?.filterAlertUrgenciesDisplay, filterAlertCertainties: value?.filterAlertCertaintiesDisplay, filterAlertSeverities: value?.filterAlertSeveritiesDisplay, @@ -452,27 +451,37 @@ export function Component() { name="active" className={styles.tabPanel} > - + + + - + + +
); } -Component.displayName = 'MySubscription'; +Component.displayName = 'MySubscriptions'; diff --git a/src/views/MySubscription/styles.module.css b/src/views/MySubscriptions/styles.module.css similarity index 100% rename from src/views/MySubscription/styles.module.css rename to src/views/MySubscriptions/styles.module.css diff --git a/src/views/NewSubscriptionModal/i18n.json b/src/views/NewSubscriptionModal/i18n.json index 3236f300..a6e3ad5d 100644 --- a/src/views/NewSubscriptionModal/i18n.json +++ b/src/views/NewSubscriptionModal/i18n.json @@ -19,10 +19,10 @@ "filterRegionsPlaceholder": "All Regions", "newSubscriptionHeading": "New Subscription", "newSubscriptionTitle": "Title", - "newSubscriptionCreatedSucessfully": "Subscription created successfully.", + "newSubscriptionCreatedSuccessfully": "Subscription created successfully.", "newSubscriptionFailed": "Failed to create subscription", - "newSubscriptionLimitExceed": "You have reached the maximum limit of 10 subscriptions", + "newSubscriptionLimitExceeded": "You have reached the maximum limit of 10 subscriptions", "subscriptionUpdatedSuccessfully": "Subscription updated successfully.", "failedToUpdateSubscription": "Failed to update subscription." } -} \ No newline at end of file +} diff --git a/src/views/NewSubscriptionModal/index.tsx b/src/views/NewSubscriptionModal/index.tsx index f02342c1..35548508 100644 --- a/src/views/NewSubscriptionModal/index.tsx +++ b/src/views/NewSubscriptionModal/index.tsx @@ -308,9 +308,10 @@ function NewSubscriptionModal(props: Props) { if (!response) { return; } + if (response.ok) { alert.show( - strings.newSubscriptionCreatedSucessfully, + strings.newSubscriptionCreatedSuccessfully, { variant: 'success' }, ); onCloseModal(); @@ -319,7 +320,7 @@ function NewSubscriptionModal(props: Props) { } } else { alert.show( - strings.newSubscriptionFailed, + strings.newSubscriptionLimitExceeded, { variant: 'danger' }, ); } @@ -354,7 +355,7 @@ function NewSubscriptionModal(props: Props) { } } else { alert.show( - strings.failedToUpdateSubscription, + strings.newSubscriptionLimitExceeded, { variant: 'danger' }, ); } @@ -421,9 +422,7 @@ function NewSubscriptionModal(props: Props) { }, }).then(() => { if (onSuccess) { - ( - onSuccess() - ); + onSuccess(); } }); } else { From 8f03ef65c62767a976c11cccc1236ae53e863fee Mon Sep 17 00:00:00 2001 From: barshathakuri Date: Fri, 6 Dec 2024 10:02:36 +0545 Subject: [PATCH 27/30] Copy patches folder in Dockerfile --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 6a0bc2a8..d3b7e019 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,7 @@ RUN git config --global --add safe.directory /code FROM dev AS builder COPY ./package.json ./pnpm-lock.yaml /code/ +COPY ./patches /code/patches/ # TODO: patches are not working with this? RUN pnpm install From 6aad3dd443770966d1b5d00beb20cfbd37f618e8 Mon Sep 17 00:00:00 2001 From: barshathakuri Date: Fri, 6 Dec 2024 11:27:14 +0545 Subject: [PATCH 28/30] Fix subscription styling --- .../SubscriptionDetail/index.tsx | 16 +++++----- src/views/MySubscriptions/index.tsx | 30 +++++++++---------- src/views/NewSubscriptionModal/index.tsx | 3 +- 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/src/views/MySubscriptions/SubscriptionDetail/index.tsx b/src/views/MySubscriptions/SubscriptionDetail/index.tsx index 842a3acc..26df5315 100644 --- a/src/views/MySubscriptions/SubscriptionDetail/index.tsx +++ b/src/views/MySubscriptions/SubscriptionDetail/index.tsx @@ -171,7 +171,6 @@ export function Component() { - - + /> +
); diff --git a/src/views/MySubscriptions/index.tsx b/src/views/MySubscriptions/index.tsx index 6d81b0ec..65ed4eab 100644 --- a/src/views/MySubscriptions/index.tsx +++ b/src/views/MySubscriptions/index.tsx @@ -454,14 +454,13 @@ export function Component() { - - + /> + - -
+ /> + diff --git a/src/views/NewSubscriptionModal/index.tsx b/src/views/NewSubscriptionModal/index.tsx index 35548508..4203dce2 100644 --- a/src/views/NewSubscriptionModal/index.tsx +++ b/src/views/NewSubscriptionModal/index.tsx @@ -240,7 +240,6 @@ const formSchema: FormSchema = { }, notifyByEmail: { required: true, - requiredValidation: requiredCondition, }, emailFrequency: { required: !!value?.notifyByEmail, @@ -278,7 +277,7 @@ function NewSubscriptionModal(props: Props) { filterAlertCountry: subscription?.filterAlertCountry, filterAlertAdmin1s: subscription?.filterAlertAdmin1s ?? [], - notifyByEmail: subscription?.notifyByEmail, + notifyByEmail: subscription?.notifyByEmail ?? false, emailFrequency: subscription?.emailFrequency ?? undefined, }), [subscription]); From 7166d5467451cc903c9d285140d5914d46e1b8e9 Mon Sep 17 00:00:00 2001 From: barshathakuri Date: Fri, 6 Dec 2024 14:30:33 +0545 Subject: [PATCH 29/30] Error message fixes --- README.md | 3 +++ src/views/MySubscriptions/index.tsx | 2 +- src/views/NewSubscriptionModal/index.tsx | 26 +++++++++++++++--------- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e43815cf..422d95da 100644 --- a/README.md +++ b/README.md @@ -71,5 +71,8 @@ docker-compose up └── index.tsx (Defines root layout and requests fetched for DomainContext) ``` +## IFRC Alert Hub backend +The backend that serves the frontend application is maintained in a separate [repository](https://github.com/IFRCGo/alert-hub-backend). + ## External facing API Here is the documentation for [Alert Hub GraphQL Client Usage Guide](./APIDOCS.md) diff --git a/src/views/MySubscriptions/index.tsx b/src/views/MySubscriptions/index.tsx index 65ed4eab..9f0fcaa3 100644 --- a/src/views/MySubscriptions/index.tsx +++ b/src/views/MySubscriptions/index.tsx @@ -209,7 +209,7 @@ export function Component() { return; } if (response.ok) { - if (response.result) { + if (response.result?.isActive === true) { alert.show( strings.subscriptionUnarchived, { variant: 'success' }, diff --git a/src/views/NewSubscriptionModal/index.tsx b/src/views/NewSubscriptionModal/index.tsx index 4203dce2..4b13e7dc 100644 --- a/src/views/NewSubscriptionModal/index.tsx +++ b/src/views/NewSubscriptionModal/index.tsx @@ -40,6 +40,7 @@ import { FilteredAdminListQueryVariables, UpdateSubscriptionMutation, UpdateSubscriptionMutationVariables, + UserAlertSubscriptionEmailFrequencyEnum, UserAlertSubscriptionInput, } from '#generated/types/graphql'; import useAlert from '#hooks/useAlert'; @@ -111,6 +112,7 @@ mutation CreateUserAlertSubscription( data: $data, ) { ok + errors result { id name @@ -240,6 +242,7 @@ const formSchema: FormSchema = { }, notifyByEmail: { required: true, + defaultValue: false, }, emailFrequency: { required: !!value?.notifyByEmail, @@ -277,8 +280,9 @@ function NewSubscriptionModal(props: Props) { filterAlertCountry: subscription?.filterAlertCountry, filterAlertAdmin1s: subscription?.filterAlertAdmin1s ?? [], - notifyByEmail: subscription?.notifyByEmail ?? false, - emailFrequency: subscription?.emailFrequency ?? undefined, + notifyByEmail: subscription?.notifyByEmail, + emailFrequency: subscription?.emailFrequency + ?? UserAlertSubscriptionEmailFrequencyEnum.Monthly, }), [subscription]); const { @@ -318,10 +322,11 @@ function NewSubscriptionModal(props: Props) { onSuccess(); } } else { - alert.show( - strings.newSubscriptionLimitExceeded, - { variant: 'danger' }, - ); + const errorMessages = response?.errors + ?.map((error: { messages: string; }) => error.messages) + .filter((message: string) => message) + .join(', '); + alert.show(errorMessages, { variant: 'danger' }); } }, onError: () => { @@ -353,10 +358,11 @@ function NewSubscriptionModal(props: Props) { onSuccess(); } } else { - alert.show( - strings.newSubscriptionLimitExceeded, - { variant: 'danger' }, - ); + const errorMessages = response?.errors + ?.map((error: { messages: string; }) => error.messages) + .filter((message: string) => message) + .join(', '); + alert.show(errorMessages, { variant: 'danger' }); } }, onError: () => { From a8234bfe6421534c8b9f508a98ab7f7a7ae92028 Mon Sep 17 00:00:00 2001 From: barshathakuri Date: Fri, 6 Dec 2024 15:03:15 +0545 Subject: [PATCH 30/30] Fix subscription limit --- src/views/NewSubscriptionModal/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/views/NewSubscriptionModal/index.tsx b/src/views/NewSubscriptionModal/index.tsx index 4b13e7dc..3e3c833c 100644 --- a/src/views/NewSubscriptionModal/index.tsx +++ b/src/views/NewSubscriptionModal/index.tsx @@ -242,7 +242,6 @@ const formSchema: FormSchema = { }, notifyByEmail: { required: true, - defaultValue: false, }, emailFrequency: { required: !!value?.notifyByEmail, @@ -280,7 +279,7 @@ function NewSubscriptionModal(props: Props) { filterAlertCountry: subscription?.filterAlertCountry, filterAlertAdmin1s: subscription?.filterAlertAdmin1s ?? [], - notifyByEmail: subscription?.notifyByEmail, + notifyByEmail: subscription?.notifyByEmail ?? false, emailFrequency: subscription?.emailFrequency ?? UserAlertSubscriptionEmailFrequencyEnum.Monthly, }), [subscription]);